Decouple plugins from their "jar" external form

Until now all the server-side plugins have been associated
to a single jar file in the /plugin directory.

As first step to allow different forms of plugins
(e.g. script files, directories or anything else that
can provide classes and resources) we need to de-couple
the underlying Jar file from the server side plugin.

We introduce the concept of "plugin-scanner" as the interface
to scan the external form to discover:
- plugin classes
- plugin resources
- plugin meta-data (i.e. Manifest)

Change-Id: I769595a030545a5f272f453c3cf435b74719e1e7
This commit is contained in:
Luca Milanesio
2014-01-29 23:53:00 +00:00
parent 0927a52b64
commit a0c3ba5a4d
10 changed files with 320 additions and 46 deletions

View File

@@ -29,6 +29,7 @@ import com.google.gerrit.server.documentation.MarkdownFormatter;
import com.google.gerrit.server.plugins.Plugin;
import com.google.gerrit.server.plugins.PluginsCollection;
import com.google.gerrit.server.plugins.ReloadPluginListener;
import com.google.gerrit.server.plugins.ServerPlugin;
import com.google.gerrit.server.plugins.StartPluginListener;
import com.google.gerrit.server.ssh.SshInfo;
import com.google.gwtexpui.server.CacheHeaders;
@@ -268,7 +269,7 @@ class HttpPluginServlet extends HttpServlet
}
if (file.startsWith(holder.staticPrefix)) {
JarFile jar = holder.plugin.getJarFile();
JarFile jar = jarFileOf(holder.plugin);
if (jar != null) {
JarEntry entry = jar.getJarEntry(file);
if (exists(entry)) {
@@ -286,7 +287,7 @@ class HttpPluginServlet extends HttpServlet
} else if (file.startsWith(holder.docPrefix) && file.endsWith("/")) {
res.sendRedirect(uri + "index.html");
} else if (file.startsWith(holder.docPrefix)) {
JarFile jar = holder.plugin.getJarFile();
JarFile jar = jarFileOf(holder.plugin);
JarEntry entry = jar.getJarEntry(file);
if (!exists(entry)) {
entry = findSource(jar, file);
@@ -632,6 +633,14 @@ class HttpPluginServlet extends HttpServlet
return data;
}
private static JarFile jarFileOf(Plugin plugin) {
if(plugin instanceof ServerPlugin) {
return ((ServerPlugin) plugin).getJarFile();
} else {
return null;
}
}
private static class PluginHolder {
final Plugin plugin;
final GuiceFilter filter;
@@ -648,7 +657,7 @@ class HttpPluginServlet extends HttpServlet
}
private static String getPrefix(Plugin plugin, String attr, String def) {
JarFile jarFile = plugin.getJarFile();
JarFile jarFile = jarFileOf(plugin);
if (jarFile == null) {
return def;
}

View File

@@ -23,7 +23,7 @@ import com.google.common.collect.Sets;
import com.google.gerrit.extensions.annotations.Export;
import com.google.gerrit.extensions.annotations.ExtensionPoint;
import com.google.gerrit.extensions.annotations.Listen;
import com.google.gerrit.server.plugins.JarScanner.ExtensionMetaData;
import com.google.gerrit.server.plugins.PluginContentScanner.ExtensionMetaData;
import com.google.inject.AbstractModule;
import com.google.inject.Module;
import com.google.inject.Scopes;
@@ -34,12 +34,11 @@ import java.lang.reflect.ParameterizedType;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarFile;
class AutoRegisterModules {
private final String pluginName;
private final PluginGuiceEnvironment env;
private final JarFile jarFile;
private final PluginContentScanner scanner;
private final ClassLoader classLoader;
private final ModuleGenerator sshGen;
private final ModuleGenerator httpGen;
@@ -53,11 +52,11 @@ class AutoRegisterModules {
AutoRegisterModules(String pluginName,
PluginGuiceEnvironment env,
JarFile jarFile,
PluginContentScanner scanner,
ClassLoader classLoader) {
this.pluginName = pluginName;
this.env = env;
this.jarFile = jarFile;
this.scanner = scanner;
this.classLoader = classLoader;
this.sshGen = env.hasSshModule() ? env.newSshModuleGenerator() : null;
this.httpGen = env.hasHttpModule() ? env.newHttpModuleGenerator() : null;
@@ -111,7 +110,7 @@ class AutoRegisterModules {
private void scan() throws InvalidPluginException {
Map<Class<? extends Annotation>, Iterable<ExtensionMetaData>> extensions =
JarScanner.scan(jarFile, pluginName, Arrays.asList(Export.class, Listen.class));
scanner.scan(pluginName, Arrays.asList(Export.class, Listen.class));
for (ExtensionMetaData export : extensions.get(Export.class)) {
export(export);
}
@@ -123,18 +122,18 @@ class AutoRegisterModules {
private void export(ExtensionMetaData def) throws InvalidPluginException {
Class<?> clazz;
try {
clazz = Class.forName(def.getClassName(), false, classLoader);
clazz = Class.forName(def.className, false, classLoader);
} catch (ClassNotFoundException err) {
throw new InvalidPluginException(String.format(
"Cannot load %s with @Export(\"%s\")",
def.getClassName(), def.getAnnotationValue()), err);
def.className, def.annotationValue), err);
}
Export export = clazz.getAnnotation(Export.class);
if (export == null) {
PluginLoader.log.warn(String.format(
"In plugin %s asm incorrectly parsed %s with @Export(\"%s\")",
pluginName, clazz.getName(), def.getAnnotationValue()));
pluginName, clazz.getName(), def.annotationValue));
return;
}
@@ -162,11 +161,11 @@ class AutoRegisterModules {
private void listen(ExtensionMetaData def) throws InvalidPluginException {
Class<?> clazz;
try {
clazz = Class.forName(def.getClassName(), false, classLoader);
clazz = Class.forName(def.className, false, classLoader);
} catch (ClassNotFoundException err) {
throw new InvalidPluginException(String.format(
"Cannot load %s with @Listen",
def.getClassName()), err);
def.className), err);
}
Listen listen = clazz.getAnnotation(Listen.class);

View File

@@ -24,6 +24,7 @@ import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
@@ -38,6 +39,7 @@ import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
@@ -46,10 +48,12 @@ import java.util.Collections;
import java.util.Enumeration;
import java.util.Map;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
public class JarScanner {
public class JarScanner implements PluginContentScanner {
private static final int SKIP_ALL = ClassReader.SKIP_CODE
| ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
private static final Function<ClassData, ExtensionMetaData> CLASS_DATA_TO_EXTENSION_META_DATA =
@@ -61,27 +65,19 @@ public class JarScanner {
}
};
public static class ExtensionMetaData {
private final String className;
private final String annotationValue;
private final JarFile jarFile;
private ExtensionMetaData(String className, String annotationValue) {
this.className = className;
this.annotationValue = annotationValue;
}
public String getAnnotationValue() {
return annotationValue;
}
public String getClassName() {
return className;
public JarScanner(File srcFile) throws InvalidPluginException {
try {
this.jarFile = new JarFile(srcFile);
} catch (IOException e) {
throw new InvalidPluginException("Cannot scan plugin file " + srcFile, e);
}
}
public static Map<Class<? extends Annotation>, Iterable<ExtensionMetaData>> scan(
JarFile jarFile, String pluginName,
Iterable<Class<? extends Annotation>> annotations)
@Override
public Map<Class<? extends Annotation>, Iterable<ExtensionMetaData>> scan(
String pluginName, Iterable<Class<? extends Annotation>> annotations)
throws InvalidPluginException {
Set<String> descriptors = Sets.newHashSet();
Multimap<String, JarScanner.ClassData> rawMap = ArrayListMultimap.create();
@@ -262,4 +258,62 @@ public class JarScanner {
public void visitEnd() {
}
}
@Override
public Optional<PluginEntry> getEntry(String resourcePath) throws IOException {
JarEntry jarEntry = jarFile.getJarEntry(resourcePath);
if (jarEntry == null || jarEntry.getSize() == 0) {
return Optional.absent();
}
return Optional.of(resourceOf(jarEntry));
}
@Override
public Enumeration<PluginEntry> entries() {
return Collections.enumeration(Lists.transform(
Collections.list(jarFile.entries()),
new Function<JarEntry, PluginEntry>() {
public PluginEntry apply(JarEntry jarEntry) {
try {
return resourceOf(jarEntry);
} catch (IOException e) {
throw new IllegalArgumentException("Cannot convert jar entry "
+ jarEntry + " to a resource", e);
}
}
}));
}
@Override
public InputStream getInputStream(PluginEntry entry)
throws IOException {
return jarFile.getInputStream(jarFile
.getEntry(entry.getName()));
}
@Override
public Manifest getManifest() throws IOException {
return jarFile.getManifest();
}
private PluginEntry resourceOf(JarEntry jarEntry) throws IOException {
return new PluginEntry(jarEntry.getName(), jarEntry.getTime(),
jarEntry.getSize(), attributesOf(jarEntry));
}
private Map<Object, String> attributesOf(JarEntry jarEntry)
throws IOException {
Attributes attributes = jarEntry.getAttributes();
if (attributes == null) {
return Collections.emptyMap();
}
return Maps.transformEntries(attributes,
new Maps.EntryTransformer<Object, Object, String>() {
@Override
public String transformEntry(Object key, Object value) {
return (String) value;
}
});
}
}

View File

@@ -66,11 +66,6 @@ class JsPlugin extends Plugin {
}
}
@Override
public JarFile getJarFile() {
return null;
}
@Override
public Injector getSysInjector() {
return null;
@@ -109,4 +104,9 @@ class JsPlugin extends Plugin {
new JavaScriptPlugin(fileName));
}
}
@Override
public PluginContentScanner getContentScanner() {
return PluginContentScanner.EMPTY;
}
}

View File

@@ -137,7 +137,7 @@ public class ListPlugins implements RestReadView<TopLevelResource> {
version = p.getVersion();
disabled = p.isDisabled() ? true : null;
if (p.getJarFile() != null) {
if (p.getSrcFile() != null) {
indexUrl = String.format("plugins/%s/", p.getName());
}
}

View File

@@ -124,7 +124,7 @@ public abstract class Plugin {
abstract void stop(PluginGuiceEnvironment env);
public abstract JarFile getJarFile();
public abstract PluginContentScanner getContentScanner();
public abstract Injector getSysInjector();

View File

@@ -0,0 +1,130 @@
// Copyright (C) 2014 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.base.Optional;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Map;
import java.util.jar.Manifest;
/**
* Scans the plugin returning classes and resources.
*
* Gerrit uses the scanner to automatically discover the classes
* and resources exported by the plugin for auto discovery
* of exported SSH commands, Servlets and listeners.
*/
public interface PluginContentScanner {
/**
* Scanner without resources.
*/
PluginContentScanner EMPTY = new PluginContentScanner() {
@Override
public Manifest getManifest() throws IOException {
return new Manifest();
}
@Override
public Map<Class<? extends Annotation>, Iterable<ExtensionMetaData>> scan(
String pluginName, Iterable<Class<? extends Annotation>> annotations)
throws InvalidPluginException {
return Collections.emptyMap();
}
@Override
public Optional<PluginEntry> getEntry(String resourcePath)
throws IOException {
return Optional.absent();
}
@Override
public InputStream getInputStream(PluginEntry entry) throws IOException {
throw new FileNotFoundException("Empty plugin");
}
@Override
public Enumeration<PluginEntry> entries() {
return Collections.emptyEnumeration();
}
};
/**
* Plugin class extension meta-data
*
* Class name and annotation value of the class
* provided by a plugin to extend an existing
* extension point in Gerrit.
*/
public static class ExtensionMetaData {
public final String className;
public final String annotationValue;
public ExtensionMetaData(String className, String annotationValue) {
this.className = className;
this.annotationValue = annotationValue;
}
}
/**
* Return the plugin meta-data manifest
*
* @return Manifest of the plugin or null if plugin has no meta-data
* @throws IOException if an I/O problem occurred whilst accessing the Manifest
*/
Manifest getManifest() throws IOException;
/**
* Scans the plugin for declared public annotated classes
*
* @param pluginName the plugin name
* @param annotations annotations declared by the plugin classes
* @return map of annotations and associated plugin classes found
* @throws InvalidPluginException if the plugin is not valid or corrupted
*/
Map<Class<? extends Annotation>, Iterable<ExtensionMetaData>> scan(
String pluginName, Iterable<Class<? extends Annotation>> annotations)
throws InvalidPluginException;
/**
* Return the plugin resource associated to a path
*
* @param resourcePath full path of the resource inside the plugin package
* @return the resource object or Optional.absent() if the resource was not found
* @throws IOException if there was a problem retrieving the resource
*/
Optional<PluginEntry> getEntry(String resourcePath) throws IOException;
/**
* Return the InputStream of the resource entry
*
* @param entry resource entry inside the plugin package
* @return the resource input stream
* @throws IOException if there was an I/O problem accessing the resource
*/
InputStream getInputStream(PluginEntry entry) throws IOException;
/**
* Return all the resources inside a plugin
*
* @return the enumeration of all resources found
*/
Enumeration<PluginEntry> entries();
}

View File

@@ -0,0 +1,64 @@
// Copyright (C) 2014 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 java.util.Collections;
import java.util.Map;
/**
* Plugin static resource entry
*
* Bean representing a static resource inside a plugin.
* All static resources are available at <plugin web url>/static
* and served by the HttpPluginServlet.
*/
public class PluginEntry {
public static final String ATTR_CHARACTER_ENCODING = "Character-Encoding";
public static final String ATTR_CONTENT_TYPE = "Content-Type";
private static final Map<Object,String> EMPTY_ATTRS = Collections.emptyMap();
private final String name;
private final long time;
private final long size;
private final Map<Object, String> attrs;
public PluginEntry(String name, long time, long size,
Map<Object, String> attrs) {
this.name = name;
this.time = time;
this.size = size;
this.attrs = attrs;
}
public PluginEntry(String name, long time, long size) {
this(name, time, size, EMPTY_ATTRS);
}
public String getName() {
return name;
}
public long getTime() {
return time;
}
public long getSize() {
return size;
}
public Map<Object, String> getAttrs() {
return attrs;
}
}

View File

@@ -589,8 +589,8 @@ public class PluginLoader implements LifecycleListener {
Plugin plugin = new ServerPlugin(name, url,
pluginUserFactory.create(name),
srcJar, snapshot,
jarFile, manifest,
srcJar, snapshot, new JarFile(srcJar),
new JarScanner(srcJar),
new File(dataDir, name), type, pluginLoader,
sysModule, sshModule, httpModule);
cleanupHandles.put(plugin, new CleanupHandle(tmp, jarFile));

View File

@@ -35,12 +35,13 @@ import com.google.inject.ProvisionException;
import org.eclipse.jgit.internal.storage.file.FileSnapshot;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
class ServerPlugin extends Plugin {
public class ServerPlugin extends Plugin {
/** Unique key that changes whenever a plugin reloads. */
public static final class CacheKey {
@@ -59,6 +60,7 @@ class ServerPlugin extends Plugin {
private final JarFile jarFile;
private final Manifest manifest;
private final PluginContentScanner scanner;
private final File dataDir;
private final String pluginCanonicalWebUrl;
private final ClassLoader classLoader;
@@ -78,28 +80,39 @@ class ServerPlugin extends Plugin {
File srcJar,
FileSnapshot snapshot,
JarFile jarFile,
Manifest manifest,
PluginContentScanner scanner,
File dataDir,
ApiType apiType,
ClassLoader classLoader,
@Nullable Class<? extends Module> sysModule,
@Nullable Class<? extends Module> sshModule,
@Nullable Class<? extends Module> httpModule) {
@Nullable Class<? extends Module> httpModule)
throws InvalidPluginException {
super(name, srcJar, pluginUser, snapshot, apiType);
this.pluginCanonicalWebUrl = pluginCanonicalWebUrl;
this.jarFile = jarFile;
this.manifest = manifest;
this.scanner = scanner;
this.dataDir = dataDir;
this.classLoader = classLoader;
this.sysModule = sysModule;
this.sshModule = sshModule;
this.httpModule = httpModule;
this.manifest = getPluginManifest(scanner);
}
File getSrcJar() {
return getSrcFile();
}
private Manifest getPluginManifest(PluginContentScanner scanner)
throws InvalidPluginException {
try {
return scanner.getManifest();
} catch (IOException e) {
throw new InvalidPluginException("Cannot get plugin manifest", e);
}
}
@Nullable
public String getVersion() {
Attributes main = manifest.getMainAttributes();
@@ -136,7 +149,7 @@ class ServerPlugin extends Plugin {
AutoRegisterModules auto = null;
if (sysModule == null && sshModule == null && httpModule == null) {
auto = new AutoRegisterModules(getName(), env, jarFile, classLoader);
auto = new AutoRegisterModules(getName(), env, scanner, classLoader);
auto.discover();
}
@@ -270,4 +283,9 @@ class ServerPlugin extends Plugin {
manager.add(handle);
}
}
@Override
public PluginContentScanner getContentScanner() {
return scanner;
}
}