295 lines
11 KiB
Java
295 lines
11 KiB
Java
// Copyright (C) 2016 The Android Open Source Project
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package com.google.gerrit.server;
|
|
|
|
import com.google.gerrit.extensions.registration.DynamicMap;
|
|
import com.google.gerrit.server.plugins.DelegatingClassLoader;
|
|
import com.google.gerrit.util.cli.CmdLineParser;
|
|
import com.google.inject.Injector;
|
|
import com.google.inject.Module;
|
|
import com.google.inject.Provider;
|
|
import java.lang.ref.WeakReference;
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Map.Entry;
|
|
import java.util.WeakHashMap;
|
|
|
|
/** Helper class to define and parse options from plugins on ssh and RestAPI commands. */
|
|
public class DynamicOptions {
|
|
/**
|
|
* To provide additional options, bind a DynamicBean. For example:
|
|
*
|
|
* <pre>
|
|
* bind(com.google.gerrit.server.DynamicOptions.DynamicBean.class)
|
|
* .annotatedWith(Exports.named(com.google.gerrit.sshd.commands.Query.class))
|
|
* .to(MyOptions.class);
|
|
* </pre>
|
|
*
|
|
* To define the additional options, implement this interface. For example:
|
|
*
|
|
* <pre>
|
|
* public class MyOptions implements DynamicOptions.DynamicBean {
|
|
* {@literal @}Option(name = "--verbose", aliases = {"-v"}
|
|
* usage = "Make the operation more talkative")
|
|
* public boolean verbose;
|
|
* }
|
|
* </pre>
|
|
*
|
|
* The option will be prefixed by the plugin name. In the example above, if the plugin name was
|
|
* my-plugin, then the --verbose option as used by the caller would be --my-plugin--verbose.
|
|
*/
|
|
public interface DynamicBean {}
|
|
|
|
/**
|
|
* To provide additional options to a command in another classloader, bind a ClassNameProvider
|
|
* which provides the name of your DynamicBean in the other classLoader.
|
|
*
|
|
* <p>Do this by binding to just the name of the command you are going to bind to so that your
|
|
* classLoader does not load the command's class which likely is not in your classpath. To ensure
|
|
* that the command's class is not in your classpath, you can exclude it during your build.
|
|
*
|
|
* <p>For example:
|
|
*
|
|
* <pre>
|
|
* bind(com.google.gerrit.server.DynamicOptions.DynamicBean.class)
|
|
* .annotatedWith(Exports.named( "com.google.gerrit.plugins.otherplugin.command"))
|
|
* .to(MyOptionsClassNameProvider.class);
|
|
*
|
|
* static class MyOptionsClassNameProvider implements DynamicOptions.ClassNameProvider {
|
|
* @Override
|
|
* public String getClassName() {
|
|
* return "com.googlesource.gerrit.plugins.myplugin.CommandOptions";
|
|
* }
|
|
* }
|
|
* </pre>
|
|
*/
|
|
public interface ClassNameProvider extends DynamicBean {
|
|
String getClassName();
|
|
}
|
|
|
|
/**
|
|
* To provide additional Guice bindings for options to a command in another classloader, bind a
|
|
* ModulesClassNamesProvider which provides the name of your Modules needed for your DynamicBean
|
|
* in the other classLoader.
|
|
*
|
|
* <p>Do this by binding to the name of the command you are going to bind to and providing an
|
|
* Iterable of Module names to instantiate and add to the Injector used to instantiate the
|
|
* DynamicBean in the other classLoader. For example:
|
|
*
|
|
* <pre>
|
|
* bind(com.google.gerrit.server.DynamicOptions.DynamicBean.class)
|
|
* .annotatedWith(Exports.named(
|
|
* "com.google.gerrit.plugins.otherplugin.command"))
|
|
* .to(MyOptionsModulesClassNamesProvider.class);
|
|
*
|
|
* static class MyOptionsModulesClassNamesProvider implements DynamicOptions.ClassNameProvider {
|
|
* @Override
|
|
* public String getClassName() {
|
|
* return "com.googlesource.gerrit.plugins.myplugin.CommandOptions";
|
|
* }
|
|
* @Override
|
|
* public Iterable<String> getModulesClassNames()() {
|
|
* return "com.googlesource.gerrit.plugins.myplugin.MyOptionsModule";
|
|
* }
|
|
* }
|
|
* </pre>
|
|
*/
|
|
public interface ModulesClassNamesProvider extends ClassNameProvider {
|
|
Iterable<String> getModulesClassNames();
|
|
}
|
|
|
|
/**
|
|
* Implement this if your DynamicBean needs an opportunity to act on the Bean directly before or
|
|
* after argument parsing.
|
|
*/
|
|
public interface BeanParseListener extends DynamicBean {
|
|
void onBeanParseStart(String plugin, Object bean);
|
|
|
|
void onBeanParseEnd(String plugin, Object bean);
|
|
}
|
|
|
|
/**
|
|
* The entity which provided additional options may need a way to receive a reference to the
|
|
* DynamicBean it provided. To do so, the existing class should implement BeanReceiver (a setter)
|
|
* and then provide some way for the plugin to request its DynamicBean (a getter.) For example:
|
|
*
|
|
* <pre>
|
|
* public class Query extends SshCommand implements DynamicOptions.BeanReceiver {
|
|
* public void setDynamicBean(String plugin, DynamicOptions.DynamicBean dynamicBean) {
|
|
* dynamicBeans.put(plugin, dynamicBean);
|
|
* }
|
|
*
|
|
* public DynamicOptions.DynamicBean getDynamicBean(String plugin) {
|
|
* return dynamicBeans.get(plugin);
|
|
* }
|
|
* ...
|
|
* }
|
|
* }
|
|
* </pre>
|
|
*/
|
|
public interface BeanReceiver {
|
|
void setDynamicBean(String plugin, DynamicBean dynamicBean);
|
|
}
|
|
|
|
/**
|
|
* MergedClassloaders allow us to load classes from both plugin classloaders. Store the merged
|
|
* classloaders in a Map to avoid creating a new classloader for each invocation. Use a
|
|
* WeakHashMap to avoid leaking these MergedClassLoaders once either plugin is unloaded. Since the
|
|
* WeakHashMap only takes care of ensuring the Keys can get garbage collected, use WeakReferences
|
|
* to store the MergedClassloaders in the WeakHashMap.
|
|
*
|
|
* <p>Outter keys are the bean plugin's classloaders (the plugin being extended)
|
|
*
|
|
* <p>Inner keys are the dynamicBeans plugin's classloaders (the extending plugin)
|
|
*
|
|
* <p>The value is the MergedClassLoader representing the merging of the outter and inner key
|
|
* classloaders.
|
|
*/
|
|
protected static Map<ClassLoader, Map<ClassLoader, WeakReference<ClassLoader>>> mergedClByCls =
|
|
Collections.synchronizedMap(
|
|
new WeakHashMap<ClassLoader, Map<ClassLoader, WeakReference<ClassLoader>>>());
|
|
|
|
protected Object bean;
|
|
protected Map<String, DynamicBean> beansByPlugin;
|
|
protected Injector injector;
|
|
|
|
/**
|
|
* Internal: For Gerrit to include options from DynamicBeans, setup a DynamicMap and instantiate
|
|
* this class so the following methods can be called if desired:
|
|
*
|
|
* <pre>
|
|
* DynamicOptions pluginOptions = new DynamicOptions(bean, injector, dynamicBeans);
|
|
* pluginOptions.parseDynamicBeans(clp);
|
|
* pluginOptions.setDynamicBeans();
|
|
* pluginOptions.onBeanParseStart();
|
|
*
|
|
* // parse arguments here: clp.parseArgument(argv);
|
|
*
|
|
* pluginOptions.onBeanParseEnd();
|
|
* </pre>
|
|
*/
|
|
public DynamicOptions(Object bean, Injector injector, DynamicMap<DynamicBean> dynamicBeans) {
|
|
this.bean = bean;
|
|
this.injector = injector;
|
|
beansByPlugin = new HashMap<>();
|
|
for (String plugin : dynamicBeans.plugins()) {
|
|
Provider<DynamicBean> provider =
|
|
dynamicBeans.byPlugin(plugin).get(bean.getClass().getCanonicalName());
|
|
if (provider != null) {
|
|
beansByPlugin.put(plugin, getDynamicBean(bean, provider.get()));
|
|
}
|
|
}
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
public DynamicBean getDynamicBean(Object bean, DynamicBean dynamicBean) {
|
|
ClassLoader coreCl = getClass().getClassLoader();
|
|
ClassLoader beanCl = bean.getClass().getClassLoader();
|
|
|
|
ClassLoader loader = beanCl;
|
|
if (beanCl != coreCl) { // bean from a plugin?
|
|
ClassLoader dynamicBeanCl = dynamicBean.getClass().getClassLoader();
|
|
if (beanCl != dynamicBeanCl) { // in a different plugin?
|
|
loader = getMergedClassLoader(beanCl, dynamicBeanCl);
|
|
}
|
|
}
|
|
|
|
String className = null;
|
|
if (dynamicBean instanceof ClassNameProvider) {
|
|
className = ((ClassNameProvider) dynamicBean).getClassName();
|
|
} else if (loader != beanCl) { // in a different plugin?
|
|
className = dynamicBean.getClass().getCanonicalName();
|
|
}
|
|
|
|
if (className != null) {
|
|
try {
|
|
List<Module> modules = new ArrayList<>();
|
|
Injector modulesInjector = injector;
|
|
if (dynamicBean instanceof ModulesClassNamesProvider) {
|
|
modulesInjector = injector.createChildInjector();
|
|
for (String moduleName :
|
|
((ModulesClassNamesProvider) dynamicBean).getModulesClassNames()) {
|
|
Class<Module> mClass = (Class<Module>) loader.loadClass(moduleName);
|
|
modules.add(modulesInjector.getInstance(mClass));
|
|
}
|
|
}
|
|
return modulesInjector
|
|
.createChildInjector(modules)
|
|
.getInstance((Class<DynamicOptions.DynamicBean>) loader.loadClass(className));
|
|
} catch (ClassNotFoundException e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
|
|
return dynamicBean;
|
|
}
|
|
|
|
protected ClassLoader getMergedClassLoader(ClassLoader beanCl, ClassLoader dynamicBeanCl) {
|
|
Map<ClassLoader, WeakReference<ClassLoader>> mergedClByCl = mergedClByCls.get(beanCl);
|
|
if (mergedClByCl == null) {
|
|
mergedClByCl = Collections.synchronizedMap(new WeakHashMap<>());
|
|
mergedClByCls.put(beanCl, mergedClByCl);
|
|
}
|
|
WeakReference<ClassLoader> mergedClRef = mergedClByCl.get(dynamicBeanCl);
|
|
ClassLoader mergedCl = null;
|
|
if (mergedClRef != null) {
|
|
mergedCl = mergedClRef.get();
|
|
}
|
|
if (mergedCl == null) {
|
|
mergedCl = new DelegatingClassLoader(beanCl, dynamicBeanCl);
|
|
mergedClByCl.put(dynamicBeanCl, new WeakReference<>(mergedCl));
|
|
}
|
|
return mergedCl;
|
|
}
|
|
|
|
public void parseDynamicBeans(CmdLineParser clp) {
|
|
for (Entry<String, DynamicBean> e : beansByPlugin.entrySet()) {
|
|
clp.parseWithPrefix("--" + e.getKey(), e.getValue());
|
|
}
|
|
}
|
|
|
|
public void setDynamicBeans() {
|
|
if (bean instanceof BeanReceiver) {
|
|
BeanReceiver receiver = (BeanReceiver) bean;
|
|
for (Entry<String, DynamicBean> e : beansByPlugin.entrySet()) {
|
|
receiver.setDynamicBean(e.getKey(), e.getValue());
|
|
}
|
|
}
|
|
}
|
|
|
|
public void onBeanParseStart() {
|
|
for (Entry<String, DynamicBean> e : beansByPlugin.entrySet()) {
|
|
DynamicBean instance = e.getValue();
|
|
if (instance instanceof BeanParseListener) {
|
|
BeanParseListener listener = (BeanParseListener) instance;
|
|
listener.onBeanParseStart(e.getKey(), bean);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void onBeanParseEnd() {
|
|
for (Entry<String, DynamicBean> e : beansByPlugin.entrySet()) {
|
|
DynamicBean instance = e.getValue();
|
|
if (instance instanceof BeanParseListener) {
|
|
BeanParseListener listener = (BeanParseListener) instance;
|
|
listener.onBeanParseEnd(e.getKey(), bean);
|
|
}
|
|
}
|
|
}
|
|
}
|