summaryrefslogtreecommitdiffstats
path: root/java/com/google/gerrit/server/DynamicOptions.java
blob: 3759f092415b2d43752c92d9f6e23971a6470f68 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
// 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:
   *
   * <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>
   *
   * 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.
   *
   * <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 {
   *     @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. 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.ClassNameProvider {
   *     @Override
   *     public String getClassName() {
   *       return "com.googlesource.gerrit.plugins.myplugin.CommandOptions";
   *     }
   *     @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);
  }

  /**
   * 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<ClassLoader, Map<ClassLoader, WeakReference<ClassLoader>>>());

  protected Object bean;
  protected Map<String, DynamicBean> beansByPlugin;
  protected Injector injector;

  /**
   * 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(bean, injector, dynamicBeans);
   *    pluginOptions.parseDynamicBeans(clp);
   *    pluginOptions.setDynamicBeans();
   *    pluginOptions.onBeanParseStart();
   *
   *    // parse arguments here:  clp.parseArgument(argv);
   *
   *    pluginOptions.onBeanParseEnd();
   * </pre>
   */
  public DynamicOptions(Object bean, Injector injector, DynamicMap<DynamicBean> dynamicBeans) {
    this.bean = bean;
    this.injector = injector;
    beansByPlugin = new HashMap<>();
    for (String plugin : dynamicBeans.plugins()) {
      Provider<DynamicBean> provider =
          dynamicBeans.byPlugin(plugin).get(bean.getClass().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));
          }
        }
        return modulesInjector
            .createChildInjector(modules)
            .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 (Entry<String, DynamicBean> e : beansByPlugin.entrySet()) {
      clp.parseWithPrefix("--" + e.getKey(), e.getValue());
    }
  }

  public void setDynamicBeans() {
    if (bean instanceof BeanReceiver) {
      BeanReceiver receiver = (BeanReceiver) bean;
      for (Entry<String, DynamicBean> e : beansByPlugin.entrySet()) {
        receiver.setDynamicBean(e.getKey(), e.getValue());
      }
    }
  }

  public void onBeanParseStart() {
    for (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 (Entry<String, DynamicBean> e : beansByPlugin.entrySet()) {
      DynamicBean instance = e.getValue();
      if (instance instanceof BeanParseListener) {
        BeanParseListener listener = (BeanParseListener) instance;
        listener.onBeanParseEnd(e.getKey(), bean);
      }
    }
  }
}