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
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
index 45b55c1..215736e 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
@@ -29,6 +29,7 @@
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 @@
}
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 @@
} 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 @@
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 @@
}
private static String getPrefix(Plugin plugin, String attr, String def) {
- JarFile jarFile = plugin.getJarFile();
+ JarFile jarFile = jarFileOf(plugin);
if (jarFile == null) {
return def;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
index bd8a524..6f1204b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
@@ -23,7 +23,7 @@
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.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 @@
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 @@
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 @@
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 @@
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);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java
index c4fe1ec..6ce3464 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java
@@ -24,6 +24,7 @@
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.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.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 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 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;
+ }
+ });
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JsPlugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JsPlugin.java
index ff72576..e21bb75 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JsPlugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JsPlugin.java
@@ -67,11 +67,6 @@
}
@Override
- public JarFile getJarFile() {
- return null;
- }
-
- @Override
public Injector getSysInjector() {
return null;
}
@@ -109,4 +104,9 @@
new JavaScriptPlugin(fileName));
}
}
+
+ @Override
+ public PluginContentScanner getContentScanner() {
+ return PluginContentScanner.EMPTY;
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java
index 577ee0c..c5611dd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java
@@ -137,7 +137,7 @@
version = p.getVersion();
disabled = p.isDisabled() ? true : null;
- if (p.getJarFile() != null) {
+ if (p.getSrcFile() != null) {
indexUrl = String.format("plugins/%s/", p.getName());
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
index 988669a..aeafc5be 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
@@ -124,7 +124,7 @@
abstract void stop(PluginGuiceEnvironment env);
- public abstract JarFile getJarFile();
+ public abstract PluginContentScanner getContentScanner();
public abstract Injector getSysInjector();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginContentScanner.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginContentScanner.java
new file mode 100644
index 0000000..0228509
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginContentScanner.java
@@ -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();
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginEntry.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginEntry.java
new file mode 100644
index 0000000..6022f02
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginEntry.java
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
index c2c8d03..b56a10c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -589,8 +589,8 @@
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));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java
index 8dae5ae..052cdc7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java
@@ -35,12 +35,13 @@
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 @@
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 @@
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 @@
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 @@
manager.add(handle);
}
}
+
+ @Override
+ public PluginContentScanner getContentScanner() {
+ return scanner;
+ }
}