Delegate dynamicBeans for external plugins

When dynamicbeans are defined by an external plugin on commands from a
different plugin, instantiate the dynamic bean with a delegating
classloader which uses the command's classloader as the parent and
resolves resources via the dynamicbean's classloader.  Also instantiate
the dynamicbeans using the injector from the command.

Change-Id: If2cff8235a9680eb64c58b77f2d482c5896baf0f
This commit is contained in:
Martin Fick
2016-11-02 18:29:32 -06:00
committed by Zac Livingston
parent b32d6fcfe4
commit 4c4acd6796
4 changed files with 114 additions and 11 deletions

View File

@@ -36,6 +36,7 @@ import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gwtexpui.server.CacheHeaders;
import com.google.inject.Inject;
import com.google.inject.Injector;
import java.io.IOException;
import java.io.StringWriter;
import java.util.HashSet;
@@ -51,11 +52,16 @@ class ParameterParser {
ImmutableSet.of("pp", "prettyPrint", "strict", "callback", "alt", "fields");
private final CmdLineParser.Factory parserFactory;
private final Injector injector;
private final DynamicMap<DynamicOptions.DynamicBean> dynamicBeans;
@Inject
ParameterParser(CmdLineParser.Factory pf, DynamicMap<DynamicOptions.DynamicBean> dynamicBeans) {
ParameterParser(
CmdLineParser.Factory pf,
Injector injector,
DynamicMap<DynamicOptions.DynamicBean> dynamicBeans) {
this.parserFactory = pf;
this.injector = injector;
this.dynamicBeans = dynamicBeans;
}
@@ -63,7 +69,7 @@ class ParameterParser {
T param, ListMultimap<String, String> in, HttpServletRequest req, HttpServletResponse res)
throws IOException {
CmdLineParser clp = parserFactory.create(param);
DynamicOptions pluginOptions = new DynamicOptions(param, dynamicBeans);
DynamicOptions pluginOptions = new DynamicOptions(param, injector, dynamicBeans);
pluginOptions.parseDynamicBeans(clp);
pluginOptions.setDynamicBeans();
pluginOptions.onBeanParseStart();

View File

@@ -15,7 +15,9 @@
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;
@@ -80,15 +82,16 @@ public class DynamicOptions {
void setDynamicBean(String plugin, DynamicBean dynamicBean);
}
Object bean;
Map<String, DynamicBean> beansByPlugin;
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, dynamicBeans);
* DynamicOptions pluginOptions = new DynamicOptions(bean, injector, dynamicBeans);
* pluginOptions.parseDynamicBeans(clp);
* pluginOptions.setDynamicBeans();
* pluginOptions.onBeanParseStart();
@@ -98,18 +101,40 @@ public class DynamicOptions {
* pluginOptions.onBeanParseEnd();
* </pre>
*/
public DynamicOptions(Object bean, DynamicMap<DynamicBean> dynamicBeans) {
public DynamicOptions(Object bean, Injector injector, DynamicMap<DynamicBean> dynamicBeans) {
this.bean = bean;
beansByPlugin = new HashMap<>();
this.injector = injector;
beansByPlugin = new HashMap<String, DynamicBean>();
for (String plugin : dynamicBeans.plugins()) {
Provider<DynamicBean> provider =
dynamicBeans.byPlugin(plugin).get(bean.getClass().getCanonicalName());
if (provider != null) {
beansByPlugin.put(plugin, provider.get());
beansByPlugin.put(plugin, getDynamicBean(bean, provider.get()));
}
}
}
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());

View File

@@ -0,0 +1,69 @@
// 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.plugins;
import com.google.common.io.ByteStreams;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Enumeration;
public class DelegatingClassLoader extends ClassLoader {
public ClassLoader target;
public DelegatingClassLoader(ClassLoader parent, ClassLoader target) {
super(parent);
this.target = target;
}
public Class<?> findClass(String name) throws ClassNotFoundException {
String path = name.replace('.', '/') + ".class";
InputStream resource = target.getResourceAsStream(path);
if (resource != null) {
try {
byte[] bytes = ByteStreams.toByteArray(resource);
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
}
}
throw new ClassNotFoundException(name);
}
public URL getResource(String name) {
URL rtn = getParent().getResource(name);
if (rtn == null) {
rtn = target.getResource(name);
}
return rtn;
}
public Enumeration<URL> getResources(String name) throws IOException {
Enumeration<URL> rtn = getParent().getResources(name);
if (rtn == null) {
rtn = target.getResources(name);
}
return rtn;
}
public InputStream getResourceAsStream(String name) {
InputStream rtn = getParent().getResourceAsStream(name);
if (rtn == null) {
rtn = target.getResourceAsStream(name);
}
return rtn;
}
}

View File

@@ -35,6 +35,7 @@ import com.google.gerrit.sshd.SshScope.Context;
import com.google.gerrit.util.cli.CmdLineParser;
import com.google.gerrit.util.cli.EndOfOptionsHandler;
import com.google.inject.Inject;
import com.google.inject.Injector;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
@@ -86,13 +87,15 @@ public abstract class BaseCommand implements Command {
@Inject private SshScope.Context context;
@Inject private DynamicMap<DynamicOptions.DynamicBean> dynamicBeans;
/** Commands declared by a plugin can be scoped by the plugin name. */
@Inject(optional = true)
@PluginName
private String pluginName;
@Inject private Injector injector;
@Inject private DynamicMap<DynamicOptions.DynamicBean> dynamicBeans = null;
/** The task, as scheduled on a worker thread. */
private final AtomicReference<Future<?>> task;
@@ -197,7 +200,7 @@ public abstract class BaseCommand implements Command {
*/
protected void parseCommandLine(Object options) throws UnloggedFailure {
final CmdLineParser clp = newCmdLineParser(options);
DynamicOptions pluginOptions = new DynamicOptions(options, dynamicBeans);
DynamicOptions pluginOptions = new DynamicOptions(options, injector, dynamicBeans);
pluginOptions.parseDynamicBeans(clp);
pluginOptions.setDynamicBeans();
pluginOptions.onBeanParseStart();