// 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: * *
* bind(com.google.gerrit.server.DynamicOptions.DynamicBean.class) * .annotatedWith(Exports.named(com.google.gerrit.sshd.commands.Query.class)) * .to(MyOptions.class); ** * To define the additional options, implement this interface. For example: * *
* public class MyOptions implements DynamicOptions.DynamicBean { * {@literal @}Option(name = "--verbose", aliases = {"-v"} * usage = "Make the operation more talkative") * public boolean verbose; * } ** * 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. * *
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. * *
For example: * *
* 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"; * } * } **/ 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. * *
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: * *
* 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*/ public interface ModulesClassNamesProvider extends ClassNameProvider { IterablegetModulesClassNames()() { * return "com.googlesource.gerrit.plugins.myplugin.MyOptionsModule"; * } * } *
* 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); * } * ... * } * } **/ 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. * *
Outter keys are the bean plugin's classloaders (the plugin being extended) * *
Inner keys are the dynamicBeans plugin's classloaders (the extending plugin) * *
The value is the MergedClassLoader representing the merging of the outter and inner key
* classloaders.
*/
protected static Map
* DynamicOptions pluginOptions = new DynamicOptions(bean, injector, dynamicBeans);
* pluginOptions.parseDynamicBeans(clp);
* pluginOptions.setDynamicBeans();
* pluginOptions.onBeanParseStart();
*
* // parse arguments here: clp.parseArgument(argv);
*
* pluginOptions.onBeanParseEnd();
*
*/
public DynamicOptions(Object bean, Injector injector, DynamicMap