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 <luca.milanesio@gmail.com>
This commit is contained in:
Luca Milanesio 2012-09-25 14:26:43 +01:00 committed by Shawn O. Pearce
parent f121c1bff1
commit 737285df3f
15 changed files with 269 additions and 42 deletions

View File

@ -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
---------

View File

@ -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() {

View File

@ -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() {

View File

@ -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 {

View File

@ -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() {

View File

@ -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() {

View File

@ -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 {

View File

@ -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<InitStep> getInitSteps() {
List<File> jars = scanJarsInPluginsDirectory();
ArrayList<InitStep> pluginsInitSteps = new ArrayList<InitStep>();
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<? extends InitStep> initStepClass =
(Class<? extends InitStep>) 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<File> 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<File>() {
@Override
public int compare(File o1, File o2) {
return o1.getName().compareTo(o2.getName());
}
});
return Arrays.asList(matches);
}
}

View File

@ -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 {

View File

@ -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;
}

View File

@ -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 {

View File

@ -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<String> all = new ArrayList<String>();
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);
}
}
<T extends Enum<?>> void set(final String name, final T value) {
public <T extends Enum<?>> 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 extends Enum<?>> T select(final String title, final String name,
public <T extends Enum<?>> T select(final String title, final String name,
final T defValue) {
return select(title, name, defValue, false);
}
<T extends Enum<?>> T select(final String title, final String name,
public <T extends Enum<?>> 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<String> 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);
}
}

View File

@ -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)) {

View File

@ -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);
}
};

View File

@ -45,6 +45,18 @@ limitations under the License.
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-pgm</artifactId>
<version>${project.version}</version>
<exclusions>
<exclusion>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>servlet-api</artifactId>