LCOV - code coverage report
Current view: top level - server - DynamicOptions.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 73 91 80.2 %
Date: 2022-11-19 15:00:39 Functions: 12 13 92.3 %

          Line data    Source code
       1             : // Copyright (C) 2016 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.server;
      16             : 
      17             : import com.google.gerrit.extensions.registration.DynamicMap;
      18             : import com.google.gerrit.lifecycle.LifecycleManager;
      19             : import com.google.gerrit.server.plugins.DelegatingClassLoader;
      20             : import com.google.gerrit.util.cli.CmdLineParser;
      21             : import com.google.inject.Injector;
      22             : import com.google.inject.Module;
      23             : import com.google.inject.Provider;
      24             : import java.lang.ref.WeakReference;
      25             : import java.util.ArrayList;
      26             : import java.util.Collections;
      27             : import java.util.HashMap;
      28             : import java.util.List;
      29             : import java.util.Map;
      30             : import java.util.WeakHashMap;
      31             : 
      32             : /** Helper class to define and parse options from plugins on ssh and RestAPI commands. */
      33             : public class DynamicOptions implements AutoCloseable {
      34             :   /**
      35             :    * To provide additional options, bind a DynamicBean. For example:
      36             :    *
      37             :    * <pre>
      38             :    *   bind(com.google.gerrit.server.DynamicOptions.DynamicBean.class)
      39             :    *       .annotatedWith(Exports.named(com.google.gerrit.sshd.commands.Query.class))
      40             :    *       .to(MyOptions.class);
      41             :    * </pre>
      42             :    *
      43             :    * To define the additional options, implement this interface. For example:
      44             :    *
      45             :    * <pre>
      46             :    *   public class MyOptions implements DynamicOptions.DynamicBean {
      47             :    *     {@literal @}Option(name = "--verbose", aliases = {"-v"}
      48             :    *             usage = "Make the operation more talkative")
      49             :    *     public boolean verbose;
      50             :    *   }
      51             :    * </pre>
      52             :    *
      53             :    * <p>The option will be prefixed by the plugin name. In the example above, if the plugin name was
      54             :    * my-plugin, then the --verbose option as used by the caller would be --my-plugin--verbose.
      55             :    *
      56             :    * <p>Additional options can be annotated with @RequiresOption which will cause them to be ignored
      57             :    * unless the required option is present. For example:
      58             :    *
      59             :    * <pre>
      60             :    *   {@literal @}RequiresOptions("--help")
      61             :    *   {@literal @}Option(name = "--help-as-json",
      62             :    *           usage = "display help text in json format")
      63             :    *   public boolean displayHelpAsJson;
      64             :    * </pre>
      65             :    */
      66             :   public interface DynamicBean {}
      67             : 
      68             :   /**
      69             :    * To provide additional options to a command in another classloader, bind a ClassNameProvider
      70             :    * which provides the name of your DynamicBean in the other classLoader.
      71             :    *
      72             :    * <p>Do this by binding to just the name of the command you are going to bind to so that your
      73             :    * classLoader does not load the command's class which likely is not in your classpath. To ensure
      74             :    * that the command's class is not in your classpath, you can exclude it during your build.
      75             :    *
      76             :    * <p>For example:
      77             :    *
      78             :    * <pre>
      79             :    *   bind(com.google.gerrit.server.DynamicOptions.DynamicBean.class)
      80             :    *       .annotatedWith(Exports.named( "com.google.gerrit.plugins.otherplugin.command"))
      81             :    *       .to(MyOptionsClassNameProvider.class);
      82             :    *
      83             :    *   static class MyOptionsClassNameProvider implements DynamicOptions.ClassNameProvider {
      84             :    *     {@literal @}Override
      85             :    *     public String getClassName() {
      86             :    *       return "com.googlesource.gerrit.plugins.myplugin.CommandOptions";
      87             :    *     }
      88             :    *   }
      89             :    * </pre>
      90             :    */
      91             :   public interface ClassNameProvider extends DynamicBean {
      92             :     String getClassName();
      93             :   }
      94             : 
      95             :   /**
      96             :    * To provide additional Guice bindings for options to a command in another classloader, bind a
      97             :    * ModulesClassNamesProvider which provides the name of your Modules needed for your DynamicBean
      98             :    * in the other classLoader.
      99             :    *
     100             :    * <p>Do this by binding to the name of the command you are going to bind to and providing an
     101             :    * Iterable of Module names to instantiate and add to the Injector used to instantiate the
     102             :    * DynamicBean in the other classLoader. This interface supports running LifecycleListeners which
     103             :    * are defined by the Modules being provided. The duration of the lifecycle starts when a ssh or
     104             :    * http request starts and ends when the request completes. For example:
     105             :    *
     106             :    * <pre>
     107             :    *   bind(com.google.gerrit.server.DynamicOptions.DynamicBean.class)
     108             :    *       .annotatedWith(Exports.named(
     109             :    *           "com.google.gerrit.plugins.otherplugin.command"))
     110             :    *       .to(MyOptionsModulesClassNamesProvider.class);
     111             :    *
     112             :    *   static class MyOptionsModulesClassNamesProvider implements DynamicOptions.ModulesClassNamesProvider {
     113             :    *     {@literal @}Override
     114             :    *     public String getClassName() {
     115             :    *       return "com.googlesource.gerrit.plugins.myplugin.CommandOptions";
     116             :    *     }
     117             :    *     {@literal @}Override
     118             :    *     public Iterable<String> getModulesClassNames()() {
     119             :    *       return "com.googlesource.gerrit.plugins.myplugin.MyOptionsModule";
     120             :    *     }
     121             :    *   }
     122             :    * </pre>
     123             :    */
     124             :   public interface ModulesClassNamesProvider extends ClassNameProvider {
     125             :     Iterable<String> getModulesClassNames();
     126             :   }
     127             : 
     128             :   /**
     129             :    * Implement this if your DynamicBean needs an opportunity to act on the Bean directly before or
     130             :    * after argument parsing.
     131             :    */
     132             :   public interface BeanParseListener extends DynamicBean {
     133             :     void onBeanParseStart(String plugin, Object bean);
     134             : 
     135             :     void onBeanParseEnd(String plugin, Object bean);
     136             :   }
     137             : 
     138             :   /**
     139             :    * The entity which provided additional options may need a way to receive a reference to the
     140             :    * DynamicBean it provided. To do so, the existing class should implement BeanReceiver (a setter)
     141             :    * and then provide some way for the plugin to request its DynamicBean (a getter.) For example:
     142             :    *
     143             :    * <pre>
     144             :    *   public class Query extends SshCommand implements DynamicOptions.BeanReceiver {
     145             :    *       public void setDynamicBean(String plugin, DynamicOptions.DynamicBean dynamicBean) {
     146             :    *         dynamicBeans.put(plugin, dynamicBean);
     147             :    *       }
     148             :    *
     149             :    *       public DynamicOptions.DynamicBean getDynamicBean(String plugin) {
     150             :    *         return dynamicBeans.get(plugin);
     151             :    *       }
     152             :    *   ...
     153             :    *   }
     154             :    * }
     155             :    * </pre>
     156             :    */
     157             :   public interface BeanReceiver {
     158             :     void setDynamicBean(String plugin, DynamicBean dynamicBean);
     159             : 
     160             :     /**
     161             :      * Returns the class that should be used for looking up exported DynamicBean bindings from
     162             :      * plugins. Override when a particular REST/SSH endpoint should respect DynamicBeans bound on a
     163             :      * different endpoint. For example, {@code GetDetail} is just a synonym for a variant of {@code
     164             :      * GetChange}, and it should respect any DynamicBeans on GetChange. GetChange}. So it should
     165             :      * return {@code GetChange.class} from this method.
     166             :      */
     167             :     default Class<? extends BeanReceiver> getExportedBeanReceiver() {
     168          69 :       return getClass();
     169             :     }
     170             :   }
     171             : 
     172             :   public interface BeanProvider {
     173             :     DynamicBean getDynamicBean(String plugin);
     174             :   }
     175             : 
     176             :   /**
     177             :    * MergedClassloaders allow us to load classes from both plugin classloaders. Store the merged
     178             :    * classloaders in a Map to avoid creating a new classloader for each invocation. Use a
     179             :    * WeakHashMap to avoid leaking these MergedClassLoaders once either plugin is unloaded. Since the
     180             :    * WeakHashMap only takes care of ensuring the Keys can get garbage collected, use WeakReferences
     181             :    * to store the MergedClassloaders in the WeakHashMap.
     182             :    *
     183             :    * <p>Outter keys are the bean plugin's classloaders (the plugin being extended)
     184             :    *
     185             :    * <p>Inner keys are the dynamicBeans plugin's classloaders (the extending plugin)
     186             :    *
     187             :    * <p>The value is the MergedClassLoader representing the merging of the outter and inner key
     188             :    * classloaders.
     189             :    */
     190          86 :   protected static Map<ClassLoader, Map<ClassLoader, WeakReference<ClassLoader>>> mergedClByCls =
     191          86 :       Collections.synchronizedMap(new WeakHashMap<>());
     192             : 
     193             :   protected Object bean;
     194             :   protected Map<String, DynamicBean> beansByPlugin;
     195             :   protected Injector injector;
     196             :   protected DynamicMap<DynamicBean> dynamicBeans;
     197             :   protected LifecycleManager lifecycleManager;
     198             : 
     199             :   /**
     200             :    * Internal: For Gerrit to include options from DynamicBeans, setup a DynamicMap and instantiate
     201             :    * this class so the following methods can be called if desired:
     202             :    *
     203             :    * <pre>
     204             :    *    DynamicOptions pluginOptions = new DynamicOptions(injector, dynamicBeans);
     205             :    *    pluginOptions.setBean(bean);
     206             :    *    pluginOptions.startLifecycleListeners();
     207             :    *    pluginOptions.parseDynamicBeans(clp);
     208             :    *    pluginOptions.setDynamicBeans();
     209             :    *    pluginOptions.onBeanParseStart();
     210             :    *
     211             :    *    // parse arguments here:  clp.parseArgument(argv);
     212             :    *
     213             :    *    pluginOptions.onBeanParseEnd();
     214             :    * </pre>
     215             :    */
     216          86 :   public DynamicOptions(Injector injector, DynamicMap<DynamicBean> dynamicBeans) {
     217          86 :     this.injector = injector;
     218          86 :     this.dynamicBeans = dynamicBeans;
     219          86 :     lifecycleManager = new LifecycleManager();
     220          86 :     beansByPlugin = new HashMap<>();
     221          86 :   }
     222             : 
     223             :   public void setBean(Object bean) {
     224          78 :     this.bean = bean;
     225             :     Class<?> beanClass =
     226          78 :         (bean instanceof BeanReceiver)
     227          69 :             ? ((BeanReceiver) bean).getExportedBeanReceiver()
     228          78 :             : bean.getClass();
     229          78 :     for (String plugin : dynamicBeans.plugins()) {
     230           5 :       Provider<DynamicBean> provider =
     231           5 :           dynamicBeans.byPlugin(plugin).get(beanClass.getCanonicalName());
     232           5 :       if (provider != null) {
     233           5 :         beansByPlugin.put(plugin, getDynamicBean(bean, provider.get()));
     234             :       }
     235           5 :     }
     236          78 :   }
     237             : 
     238             :   @SuppressWarnings("unchecked")
     239             :   public DynamicBean getDynamicBean(Object bean, DynamicBean dynamicBean) {
     240           5 :     ClassLoader coreCl = getClass().getClassLoader();
     241           5 :     ClassLoader beanCl = bean.getClass().getClassLoader();
     242             : 
     243           5 :     ClassLoader loader = beanCl;
     244           5 :     if (beanCl != coreCl) { // bean from a plugin?
     245           0 :       ClassLoader dynamicBeanCl = dynamicBean.getClass().getClassLoader();
     246           0 :       if (beanCl != dynamicBeanCl) { // in a different plugin?
     247           0 :         loader = getMergedClassLoader(beanCl, dynamicBeanCl);
     248             :       }
     249             :     }
     250             : 
     251           5 :     String className = null;
     252           5 :     if (dynamicBean instanceof ClassNameProvider) {
     253           3 :       className = ((ClassNameProvider) dynamicBean).getClassName();
     254           4 :     } else if (loader != beanCl) { // in a different plugin?
     255           0 :       className = dynamicBean.getClass().getCanonicalName();
     256             :     }
     257             : 
     258           5 :     if (className != null) {
     259             :       try {
     260           3 :         List<Module> modules = new ArrayList<>();
     261           3 :         Injector modulesInjector = injector;
     262           3 :         if (dynamicBean instanceof ModulesClassNamesProvider) {
     263           3 :           modulesInjector = injector.createChildInjector();
     264             :           for (String moduleName :
     265           3 :               ((ModulesClassNamesProvider) dynamicBean).getModulesClassNames()) {
     266           3 :             Class<Module> mClass = (Class<Module>) loader.loadClass(moduleName);
     267           3 :             modules.add(modulesInjector.getInstance(mClass));
     268           3 :           }
     269             :         }
     270           3 :         Injector childModulesInjector = modulesInjector.createChildInjector(modules);
     271           3 :         lifecycleManager.add(childModulesInjector);
     272           3 :         return childModulesInjector.getInstance(
     273           3 :             (Class<DynamicOptions.DynamicBean>) loader.loadClass(className));
     274           0 :       } catch (ClassNotFoundException e) {
     275           0 :         throw new RuntimeException(e);
     276             :       }
     277             :     }
     278             : 
     279           4 :     return dynamicBean;
     280             :   }
     281             : 
     282             :   protected ClassLoader getMergedClassLoader(ClassLoader beanCl, ClassLoader dynamicBeanCl) {
     283           0 :     Map<ClassLoader, WeakReference<ClassLoader>> mergedClByCl = mergedClByCls.get(beanCl);
     284           0 :     if (mergedClByCl == null) {
     285           0 :       mergedClByCl = Collections.synchronizedMap(new WeakHashMap<>());
     286           0 :       mergedClByCls.put(beanCl, mergedClByCl);
     287             :     }
     288           0 :     WeakReference<ClassLoader> mergedClRef = mergedClByCl.get(dynamicBeanCl);
     289           0 :     ClassLoader mergedCl = null;
     290           0 :     if (mergedClRef != null) {
     291           0 :       mergedCl = mergedClRef.get();
     292             :     }
     293           0 :     if (mergedCl == null) {
     294           0 :       mergedCl = new DelegatingClassLoader(beanCl, dynamicBeanCl);
     295           0 :       mergedClByCl.put(dynamicBeanCl, new WeakReference<>(mergedCl));
     296             :     }
     297           0 :     return mergedCl;
     298             :   }
     299             : 
     300             :   public void parseDynamicBeans(CmdLineParser clp) {
     301          78 :     for (Map.Entry<String, DynamicBean> e : beansByPlugin.entrySet()) {
     302           5 :       clp.parseWithPrefix("--" + e.getKey(), e.getValue());
     303           5 :     }
     304          78 :     clp.drainOptionQueue();
     305          78 :   }
     306             : 
     307             :   public void setDynamicBeans() {
     308          78 :     if (bean instanceof BeanReceiver) {
     309          69 :       BeanReceiver receiver = (BeanReceiver) bean;
     310          69 :       for (Map.Entry<String, DynamicBean> e : beansByPlugin.entrySet()) {
     311           4 :         receiver.setDynamicBean(e.getKey(), e.getValue());
     312           4 :       }
     313             :     }
     314          78 :   }
     315             : 
     316             :   public void startLifecycleListeners() {
     317          78 :     lifecycleManager.start();
     318          78 :   }
     319             : 
     320             :   public void stopLifecycleListeners() {
     321          86 :     lifecycleManager.stop();
     322          86 :   }
     323             : 
     324             :   public void onBeanParseStart() {
     325          78 :     for (Map.Entry<String, DynamicBean> e : beansByPlugin.entrySet()) {
     326           5 :       DynamicBean instance = e.getValue();
     327           5 :       if (instance instanceof BeanParseListener) {
     328           2 :         BeanParseListener listener = (BeanParseListener) instance;
     329           2 :         listener.onBeanParseStart(e.getKey(), bean);
     330             :       }
     331           5 :     }
     332          78 :   }
     333             : 
     334             :   public void onBeanParseEnd() {
     335          78 :     for (Map.Entry<String, DynamicBean> e : beansByPlugin.entrySet()) {
     336           5 :       DynamicBean instance = e.getValue();
     337           5 :       if (instance instanceof BeanParseListener) {
     338           2 :         BeanParseListener listener = (BeanParseListener) instance;
     339           2 :         listener.onBeanParseEnd(e.getKey(), bean);
     340             :       }
     341           5 :     }
     342          78 :   }
     343             : 
     344             :   @Override
     345             :   public void close() {
     346          86 :     stopLifecycleListeners();
     347          86 :   }
     348             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750