From 737285df3fef220f72263356f692c146d299e606 Mon Sep 17 00:00:00 2001 From: Luca Milanesio Date: Tue, 25 Sep 2012 14:26:43 +0100 Subject: [PATCH] Allow plugins to contribute InitStep to Gerrit init. Plugin has to include a class that implements com.google.gerrit.pgm.init.InitStep and specifying the class name into the Plugin MANIFEST.MF. Plugin InitStep is getting instantiated using the Gerrit PGM injector and then is able to access the same init objects. Example: -------- public class InitJira implements InitStep { private ConsoleUI ui; @Inject public InitJira(final ConsoleUI ui, final Section.Factory sections) { this.ui = ui; this.sections = sections; } @Override public void run() throws Exception { ui.header("Jira integration"); // Jira-specific init steps. Section jira = sections.get("link", "jira"); // Jira-specific link expansions } } MANIFEST.MF: ------------ Gerrit-InitStep: com.googlesource.plugins.jira.InitJira Change-Id: I84fcb9acd6845c706961657219a14570e2060b0f Signed-off-by: Luca Milanesio --- Documentation/dev-plugins.txt | 63 +++++++++ .../com/google/gerrit/pgm/init/InitAuth.java | 4 +- .../com/google/gerrit/pgm/init/InitCache.java | 2 +- .../google/gerrit/pgm/init/InitContainer.java | 2 +- .../google/gerrit/pgm/init/InitDatabase.java | 2 +- .../gerrit/pgm/init/InitGitManager.java | 2 +- .../com/google/gerrit/pgm/init/InitHttpd.java | 4 +- .../pgm/init/InitPluginStepsLoader.java | 132 ++++++++++++++++++ .../google/gerrit/pgm/init/InitPlugins.java | 16 ++- .../google/gerrit/pgm/init/InitSendEmail.java | 2 +- .../com/google/gerrit/pgm/init/InitSshd.java | 2 +- .../com/google/gerrit/pgm/init/Section.java | 62 ++++---- .../gerrit/pgm/init/UpgradeFrom2_0_x.java | 2 +- .../gerrit/pgm/init/UpgradeFrom2_0_xTest.java | 4 +- gerrit-plugin-api/pom.xml | 12 ++ 15 files changed, 269 insertions(+), 42 deletions(-) create mode 100644 gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt index dd0c44c70f..5af9d1c40e 100644 --- a/Documentation/dev-plugins.txt +++ b/Documentation/dev-plugins.txt @@ -176,6 +176,69 @@ reload:: To reload/restart a plugin the link:cmd-plugin-reload.html[plugin reload] command can be used. +[[init_step]] +Init step +~~~~~~~~~ + +Plugins can contribute their own "init step" during the Gerrit init +wizard. This is useful for guiding the Gerrit administrator through +the settings needed by the plugin to work propertly. + +For instance plugins to integrate Jira issues to Gerrit changes may +contribute their own "init step" to allow configuring the Jira URL, +credentials and possibly verify connectivity to validate them. + +==== + Gerrit-InitStep: tld.example.project.MyInitStep +==== + +MyInitStep needs to follow the standard Gerrit InitStep syntax +and behaviour: writing to the console using the injected ConsoleUI +and accessing / changing configuration settings using Section.Factory. + +In addition to the standard Gerrit init injections, plugins receive +the @PluginName String injection containing their own plugin name. + +Bear in mind that the Plugin's InitStep class will be loaded but +the standard Gerrit runtime environment is not available and the plugin's +own Guice modules were not initialized. +This means the InitStep for a plugin is not executed in the same way that +the plugin executes within the server, and may mean a plugin author cannot +trivially reuse runtime code during init. + +For instance a plugin that wants to verify connectivity may need to statically +call the constructor of their connection class, passing in values obtained +from the Section.Factory rather than from an injected Config object. + +Plugins InitStep are executing during the "Gerrit Plugin init" phase, after +the extraction of the plugins embedded in Gerrit.war into $GERRIT_SITE/plugins +and before the DB Schema initialization or upgrade. +Plugins InitStep cannot refer to Gerrit DB Schema or any other Gerrit runtime +objects injected at startup. + +==== +public class MyInitStep implements InitStep { + private final ConsoleUI ui; + private final Section.Factory sections; + private final String pluginName; + + @Inject + public GitBlitInitStep(final ConsoleUI ui, Section.Factory sections, @PluginName String pluginName) { + this.ui = ui; + this.sections = sections; + this.pluginName = pluginName; + } + + @Override + public void run() throws Exception { + ui.header("\nMy plugin"); + + Section mySection = getSection("myplugin", null); + mySection.string("Link name", "linkname", "MyLink"); + } +} +==== + [[classpath]] Classpath --------- diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java index fa4dc142b3..ea12008d43 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java @@ -32,8 +32,8 @@ class InitAuth implements InitStep { @Inject InitAuth(final ConsoleUI ui, final Section.Factory sections) { this.ui = ui; - this.auth = sections.get("auth"); - this.ldap = sections.get("ldap"); + this.auth = sections.get("auth", null); + this.ldap = sections.get("ldap", null); } public void run() { diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitCache.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitCache.java index fb1a924c8f..2b5729f7bd 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitCache.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitCache.java @@ -31,7 +31,7 @@ class InitCache implements InitStep { @Inject InitCache(final SitePaths site, final Section.Factory sections) { this.site = site; - this.cache = sections.get("cache"); + this.cache = sections.get("cache", null); } public void run() { diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitContainer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitContainer.java index 7063f5440f..f34e22d0be 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitContainer.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitContainer.java @@ -44,7 +44,7 @@ class InitContainer implements InitStep { final Section.Factory sections) { this.ui = ui; this.site = site; - this.container = sections.get("container"); + this.container = sections.get("container", null); } public void run() throws FileNotFoundException, IOException { diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java index 479b9e6eda..4ce963a3ca 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java @@ -47,7 +47,7 @@ class InitDatabase implements InitStep { this.ui = ui; this.site = site; this.libraries = libraries; - this.database = sections.get("database"); + this.database = sections.get("database", null); } public void run() { diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitGitManager.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitGitManager.java index f0cd31fcd9..b6f8519d6c 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitGitManager.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitGitManager.java @@ -31,7 +31,7 @@ class InitGitManager implements InitStep { @Inject InitGitManager(final ConsoleUI ui, final Section.Factory sections) { this.ui = ui; - this.gerrit = sections.get("gerrit"); + this.gerrit = sections.get("gerrit", null); } public void run() { diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitHttpd.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitHttpd.java index 98d2e472d6..feb00e1d63 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitHttpd.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitHttpd.java @@ -48,8 +48,8 @@ class InitHttpd implements InitStep { this.ui = ui; this.site = site; this.flags = flags; - this.httpd = sections.get("httpd"); - this.gerrit = sections.get("gerrit"); + this.httpd = sections.get("httpd", null); + this.gerrit = sections.get("gerrit", null); } public void run() throws IOException, InterruptedException { diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java new file mode 100644 index 0000000000..08422fa284 --- /dev/null +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java @@ -0,0 +1,132 @@ +// Copyright (C) 2012 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.pgm.init; + +import com.google.gerrit.extensions.annotations.PluginName; +import com.google.gerrit.pgm.util.ConsoleUI; +import com.google.gerrit.server.config.SitePaths; +import com.google.inject.AbstractModule; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Singleton; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileFilter; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.jar.Attributes; +import java.util.jar.JarFile; + +@Singleton +public class InitPluginStepsLoader { + private final File pluginsDir; + private final Injector initInjector; + final ConsoleUI ui; + + @Inject + public InitPluginStepsLoader(final ConsoleUI ui, final SitePaths sitePaths, + final Injector initInjector) { + this.pluginsDir = sitePaths.plugins_dir; + this.initInjector = initInjector; + this.ui = ui; + } + + public Collection getInitSteps() { + List jars = scanJarsInPluginsDirectory(); + ArrayList pluginsInitSteps = new ArrayList(); + + for (File jar : jars) { + InitStep init = loadInitStep(jar); + if (init != null) { + pluginsInitSteps.add(init); + } + } + return pluginsInitSteps; + } + + private InitStep loadInitStep(File jar) { + try { + ClassLoader pluginLoader = + new URLClassLoader(new URL[] {jar.toURI().toURL()}, + InitPluginStepsLoader.class.getClassLoader()); + JarFile jarFile = new JarFile(jar); + Attributes jarFileAttributes = jarFile.getManifest().getMainAttributes(); + String initClassName = jarFileAttributes.getValue("Gerrit-InitStep"); + if (initClassName == null) { + return null; + } + @SuppressWarnings("unchecked") + Class initStepClass = + (Class) pluginLoader.loadClass(initClassName); + return getPluginInjector(jar).getInstance(initStepClass); + } catch (ClassCastException e) { + ui.message( + "WARN: InitStep from plugin %s does not implement %s (Exception: %s)", + jar.getName(), InitStep.class.getName(), e.getMessage()); + return null; + } catch (Exception e) { + ui.message( + "WARN: Cannot load and get plugin init step for %s (Exception: %s)", + jar, e.getMessage()); + return null; + } + } + + private Injector getPluginInjector(File jarFile) { + String jarFileName = jarFile.getName(); + final String pluginName = + jarFileName.substring(0, jarFileName.lastIndexOf('.')); + return initInjector.createChildInjector(new AbstractModule() { + @Override + protected void configure() { + bind(String.class).annotatedWith(PluginName.class).toInstance( + pluginName); + } + }); + } + + private List scanJarsInPluginsDirectory() { + if (pluginsDir == null || !pluginsDir.exists()) { + return Collections.emptyList(); + } + File[] matches = pluginsDir.listFiles(new FileFilter() { + @Override + public boolean accept(File pathname) { + String n = pathname.getName(); + return (n.endsWith(".jar") && pathname.isFile()); + } + }); + if (matches == null) { + ui.message("WARN: Cannot list %s", pluginsDir.getAbsolutePath()); + return Collections.emptyList(); + } + Arrays.sort(matches, new Comparator() { + @Override + public int compare(File o1, File o2) { + return o1.getName().compareTo(o2.getName()); + } + }); + return Arrays.asList(matches); + } +} diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java index 155fe4c859..aecb808130 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java @@ -25,6 +25,7 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.util.Collection; import java.util.Enumeration; import java.util.jar.Attributes; import java.util.jar.JarFile; @@ -39,17 +40,24 @@ public class InitPlugins implements InitStep { private final ConsoleUI ui; private final SitePaths site; + private InitPluginStepsLoader pluginLoader; @Inject - InitPlugins(final ConsoleUI ui, final SitePaths site) { + InitPlugins(final ConsoleUI ui, final SitePaths site, InitPluginStepsLoader pluginLoader) { this.ui = ui; this.site = site; + this.pluginLoader = pluginLoader; } @Override public void run() throws Exception { ui.header("Plugins"); + installPlugins(); + initPlugins(); + } + + private void installPlugins() throws IOException { final File myWar; try { myWar = GerritLauncher.getDistributionArchive(); @@ -127,6 +135,12 @@ public class InitPlugins implements InitStep { } } + private void initPlugins() throws Exception { + for (InitStep initStep : pluginLoader.getInitSteps()) { + initStep.run(); + } + } + private static String getVersion(final File plugin) throws IOException { final JarFile jarFile = new JarFile(plugin); try { diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSendEmail.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSendEmail.java index c5732e9a3e..e4b827d1c0 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSendEmail.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSendEmail.java @@ -34,7 +34,7 @@ class InitSendEmail implements InitStep { InitSendEmail(final ConsoleUI ui, final SitePaths site, final Section.Factory sections) { this.ui = ui; - this.sendemail = sections.get("sendemail"); + this.sendemail = sections.get("sendemail", null); this.site = site; } diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSshd.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSshd.java index bfc0eaf2ce..0c2a3c65bb 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSshd.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSshd.java @@ -45,7 +45,7 @@ class InitSshd implements InitStep { this.ui = ui; this.site = site; this.libraries = libraries; - this.sshd = sections.get("sshd"); + this.sshd = sections.get("sshd", null); } public void run() throws Exception { diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Section.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Section.java index c5f719f174..387b93a324 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Section.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Section.java @@ -25,52 +25,58 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Set; +import javax.annotation.Nullable; + /** Helper to edit a section of the configuration files. */ -class Section { - interface Factory { - Section get(String name); +public class Section { + public interface Factory { + Section get(@Assisted("section") String section, + @Assisted("subsection") String subsection); } private final InitFlags flags; private final SitePaths site; private final ConsoleUI ui; private final String section; + private final String subsection; @Inject - Section(final InitFlags flags, final SitePaths site, final ConsoleUI ui, - @Assisted final String section) { + public Section(final InitFlags flags, final SitePaths site, + final ConsoleUI ui, @Assisted("section") final String section, + @Assisted("subsection") @Nullable final String subsection) { this.flags = flags; this.site = site; this.ui = ui; this.section = section; + this.subsection = subsection; } String get(String name) { return flags.cfg.getString(section, null, name); } - void set(final String name, final String value) { + public void set(final String name, final String value) { final ArrayList all = new ArrayList(); - all.addAll(Arrays.asList(flags.cfg.getStringList(section, null, name))); + all.addAll(Arrays.asList(flags.cfg.getStringList(section, subsection, name))); if (value != null) { if (all.size() == 0 || all.size() == 1) { - flags.cfg.setString(section, null, name, value); + flags.cfg.setString(section, subsection, name, value); } else { all.set(0, value); - flags.cfg.setStringList(section, null, name, all); + flags.cfg.setStringList(section, subsection, name, all); } } else if (all.size() == 0) { } else if (all.size() == 1) { - flags.cfg.unset(section, null, name); + flags.cfg.unset(section, subsection, name); } else { all.remove(0); - flags.cfg.setStringList(section, null, name, all); + flags.cfg.setStringList(section, subsection, name, all); } } - > void set(final String name, final T value) { + public > void set(final String name, final T value) { if (value != null) { set(name, value.name()); } else { @@ -78,15 +84,15 @@ class Section { } } - void unset(String name) { + public void unset(String name) { set(name, (String) null); } - String string(final String title, final String name, final String dv) { + public String string(final String title, final String name, final String dv) { return string(title, name, dv, false); } - String string(final String title, final String name, final String dv, + public String string(final String title, final String name, final String dv, final boolean nullIfDefault) { final String ov = get(name); String nv = ui.readString(ov != null ? ov : dv, "%s", title); @@ -99,19 +105,19 @@ class Section { return nv; } - File path(final String title, final String name, final String defValue) { + public File path(final String title, final String name, final String defValue) { return site.resolve(string(title, name, defValue)); } - > T select(final String title, final String name, + public > T select(final String title, final String name, final T defValue) { return select(title, name, defValue, false); } - > T select(final String title, final String name, + public > T select(final String title, final String name, final T defValue, final boolean nullIfDefault) { final boolean set = get(name) != null; - T oldValue = ConfigUtil.getEnum(flags.cfg, section, null, name, defValue); + T oldValue = ConfigUtil.getEnum(flags.cfg, section, subsection, name, defValue); T newValue = ui.readEnum(oldValue, "%s", title); if (nullIfDefault && newValue == defValue) { newValue = null; @@ -126,7 +132,7 @@ class Section { return newValue; } - String select(final String title, final String name, final String dv, + public String select(final String title, final String name, final String dv, Set allowedValues) { final String ov = get(name); String nv = ui.readString(ov != null ? ov : dv, allowedValues, "%s", title); @@ -136,16 +142,16 @@ class Section { return nv; } - String password(final String username, final String password) { + public String password(final String username, final String password) { final String ov = getSecure(password); - String user = flags.sec.getString(section, null, username); + String user = flags.sec.getString(section, subsection, username); if (user == null) { user = get(username); } if (user == null) { - flags.sec.unset(section, null, password); + flags.sec.unset(section, subsection, password); return null; } @@ -165,15 +171,15 @@ class Section { return nv; } - String getSecure(String name) { - return flags.sec.getString(section, null, name); + public String getSecure(String name) { + return flags.sec.getString(section, subsection, name); } - void setSecure(String name, String value) { + public void setSecure(String name, String value) { if (value != null) { - flags.sec.setString(section, null, name, value); + flags.sec.setString(section, subsection, name, value); } else { - flags.sec.unset(section, null, name); + flags.sec.unset(section, subsection, name); } } diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java index 4a8357b512..b982ae1caf 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java @@ -117,7 +117,7 @@ class UpgradeFrom2_0_x implements InitStep { final Properties oldprop = readGerritServerProperties(); if (oldprop != null) { - final Section database = sections.get("database"); + final Section database = sections.get("database", null); String url = oldprop.getProperty("url"); if (url != null && !convertUrl(database, url)) { diff --git a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java index 493a440092..86f0260ddc 100644 --- a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java +++ b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java @@ -72,8 +72,8 @@ public class UpgradeFrom2_0_xTest extends InitTestCase { final ConsoleUI ui = createStrictMock(ConsoleUI.class); Section.Factory sections = new Section.Factory() { @Override - public Section get(String name) { - return new Section(flags, site, ui, name); + public Section get(String name, String subsection) { + return new Section(flags, site, ui, name, subsection); } }; diff --git a/gerrit-plugin-api/pom.xml b/gerrit-plugin-api/pom.xml index 0f9569364f..81a5098467 100644 --- a/gerrit-plugin-api/pom.xml +++ b/gerrit-plugin-api/pom.xml @@ -45,6 +45,18 @@ limitations under the License. ${project.version} + + com.google.gerrit + gerrit-pgm + ${project.version} + + + org.eclipse.jetty + jetty-servlet + + + + org.apache.tomcat servlet-api