| // 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(); |
| } |
| } |