blob: 7f9fbd29a36a6620cabffb30420b04f92c249278 [file] [log] [blame]
// 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.lifecycle.LifecycleManager;
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.WeakHashMap;
/** Helper class to define and parse options from plugins on ssh and RestAPI commands. */
public class DynamicOptions implements AutoCloseable {
/**
* 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>
*
* <p>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.
*
* <p>Additional options can be annotated with @RequiresOption which will cause them to be ignored
* unless the required option is present. For example:
*
* <pre>
* {@literal @}RequiresOptions("--help")
* {@literal @}Option(name = "--help-as-json",
* usage = "display help text in json format")
* public boolean displayHelpAsJson;
* </pre>
*/
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 {
* {@literal @}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. This interface supports running LifecycleListeners which
* are defined by the Modules being provided. The duration of the lifecycle starts when a ssh or
* http request starts and ends when the request completes. 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.ModulesClassNamesProvider {
* {@literal @}Override
* public String getClassName() {
* return "com.googlesource.gerrit.plugins.myplugin.CommandOptions";
* }
* {@literal @}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);
/**
* Returns the class that should be used for looking up exported DynamicBean bindings from
* plugins. Override when a particular REST/SSH endpoint should respect DynamicBeans bound on a
* different endpoint. For example, {@code GetDetail} is just a synonym for a variant of {@code
* GetChange}, and it should respect any DynamicBeans on GetChange. GetChange}. So it should
* return {@code GetChange.class} from this method.
*/
default Class<? extends BeanReceiver> getExportedBeanReceiver() {
return getClass();
}
}
public interface BeanProvider {
DynamicBean getDynamicBean(String plugin);
}
/**
* 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<>());
protected Object bean;
protected Map<String, DynamicBean> beansByPlugin;
protected Injector injector;
protected DynamicMap<DynamicBean> dynamicBeans;
protected LifecycleManager lifecycleManager;
/**
* 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(injector, dynamicBeans);
* pluginOptions.setBean(bean);
* pluginOptions.startLifecycleListeners();
* pluginOptions.parseDynamicBeans(clp);
* pluginOptions.setDynamicBeans();
* pluginOptions.onBeanParseStart();
*
* // parse arguments here: clp.parseArgument(argv);
*
* pluginOptions.onBeanParseEnd();
* </pre>
*/
public DynamicOptions(Injector injector, DynamicMap<DynamicBean> dynamicBeans) {
this.injector = injector;
this.dynamicBeans = dynamicBeans;
lifecycleManager = new LifecycleManager();
beansByPlugin = new HashMap<>();
}
public void setBean(Object bean) {
this.bean = bean;
Class<?> beanClass =
(bean instanceof BeanReceiver)
? ((BeanReceiver) bean).getExportedBeanReceiver()
: bean.getClass();
for (String plugin : dynamicBeans.plugins()) {
Provider<DynamicBean> provider =
dynamicBeans.byPlugin(plugin).get(beanClass.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));
}
}
Injector childModulesInjector = modulesInjector.createChildInjector(modules);
lifecycleManager.add(childModulesInjector);
return childModulesInjector.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 (Map.Entry<String, DynamicBean> e : beansByPlugin.entrySet()) {
clp.parseWithPrefix("--" + e.getKey(), e.getValue());
}
clp.drainOptionQueue();
}
public void setDynamicBeans() {
if (bean instanceof BeanReceiver) {
BeanReceiver receiver = (BeanReceiver) bean;
for (Map.Entry<String, DynamicBean> e : beansByPlugin.entrySet()) {
receiver.setDynamicBean(e.getKey(), e.getValue());
}
}
}
public void startLifecycleListeners() {
lifecycleManager.start();
}
public void stopLifecycleListeners() {
lifecycleManager.stop();
}
public void onBeanParseStart() {
for (Map.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 (Map.Entry<String, DynamicBean> e : beansByPlugin.entrySet()) {
DynamicBean instance = e.getValue();
if (instance instanceof BeanParseListener) {
BeanParseListener listener = (BeanParseListener) instance;
listener.onBeanParseEnd(e.getKey(), bean);
}
}
}
@Override
public void close() {
stopLifecycleListeners();
}
}