All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.server.DynamicOptions Maven / Gradle / Ivy

The newest version!
// 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:
   *
   * 
   *   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. * *

Additional options can be annotated with @RequiresOption which will cause them to be ignored * unless the required option is present. For example: * *

   *   {@literal @}RequiresOptions("--help")
   *   {@literal @}Option(name = "--help-as-json",
   *           usage = "display help text in json format")
   *   public boolean displayHelpAsJson;
   * 
*/ 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 {
   *     {@literal @}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. 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: * *

   *   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 getModulesClassNames()() {
   *       return "com.googlesource.gerrit.plugins.myplugin.MyOptionsModule";
   *     }
   *   }
   * 
*/ public interface ModulesClassNamesProvider extends ClassNameProvider { Iterable 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: * *
   *   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); /** * 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 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. * *

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>> mergedClByCls = Collections.synchronizedMap(new WeakHashMap<>()); protected Object bean; protected Map beansByPlugin; protected Injector injector; protected DynamicMap 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: * *

   *    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();
   * 
*/ public DynamicOptions(Injector injector, DynamicMap 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 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 modules = new ArrayList<>(); Injector modulesInjector = injector; if (dynamicBean instanceof ModulesClassNamesProvider) { modulesInjector = injector.createChildInjector(); for (String moduleName : ((ModulesClassNamesProvider) dynamicBean).getModulesClassNames()) { Class mClass = (Class) loader.loadClass(moduleName); modules.add(modulesInjector.getInstance(mClass)); } } Injector childModulesInjector = modulesInjector.createChildInjector(modules); lifecycleManager.add(childModulesInjector); return childModulesInjector.getInstance( (Class) loader.loadClass(className)); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } } return dynamicBean; } protected ClassLoader getMergedClassLoader(ClassLoader beanCl, ClassLoader dynamicBeanCl) { Map> mergedClByCl = mergedClByCls.get(beanCl); if (mergedClByCl == null) { mergedClByCl = Collections.synchronizedMap(new WeakHashMap<>()); mergedClByCls.put(beanCl, mergedClByCl); } WeakReference 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 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 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 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 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(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy