blob: 6267dcad0ba314a7c635e898b39e00c9af27a5f6 [file] [log] [blame]
// 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.Provider;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
/** 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 {}
/**
* 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);
}
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();
if (beanCl != coreCl) { // bean from a plugin?
ClassLoader dynamicBeanCl = dynamicBean.getClass().getClassLoader();
if (beanCl != dynamicBeanCl) { // in a different plugin?
ClassLoader mergedCL = new DelegatingClassLoader(beanCl, dynamicBeanCl);
try {
return injector
.createChildInjector()
.getInstance(
(Class<DynamicOptions.DynamicBean>)
mergedCL.loadClass(dynamicBean.getClass().getCanonicalName()));
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
return dynamicBean;
}
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);
}
}
}
}