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 : }