James Moger
2014-04-01 e5d0bacbf746e09a9194822b231cb27090f58973
Implement simple JSON-based plugin registry and install command
1 files added
6 files modified
618 ■■■■■ changed files
releases.moxie 1 ●●●● patch | view | raw | blame | history
src/main/distrib/data/gitblit.properties 20 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/manager/GitblitManager.java 36 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/manager/IPluginManager.java 48 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/manager/PluginManager.java 250 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/models/PluginRegistry.java 143 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/transport/ssh/commands/PluginDispatcher.java 120 ●●●● patch | view | raw | blame | history
releases.moxie
@@ -48,6 +48,7 @@
    - { name: 'git.sshBackend', defaultValue: 'NIO2' }
    - { name: 'git.sshCommandStartThreads', defaultValue: '2' }
    - { name: 'plugins.folder', defaultValue: '${baseFolder}/plugins' }
    - { name: 'plugins.registry', defaultValue: 'http://gitblit.github.io/gitblit-registry/plugins.json' }
}
#
src/main/distrib/data/gitblit.properties
@@ -548,6 +548,18 @@
# SINCE 1.4.0
tickets.perPage = 25
# The folder where plugins are loaded from.
#
# SINCE 1.5.0
# RESTART REQUIRED
# BASEFOLDER
plugins.folder = ${baseFolder}/plugins
# The registry of available plugins.
#
# SINCE 1.5.0
plugins.registry = http://gitblit.github.io/gitblit-registry/plugins.json
#
# Groovy Integration
#
@@ -1850,11 +1862,3 @@
# SINCE 0.5.0
# RESTART REQUIRED
server.shutdownPort = 8081
# Base folder for plugins.
# This folder may contain Gitblit plugins
#
# SINCE 1.6.0
# RESTART REQUIRED
# BASEFOLDER
plugins.folder = ${baseFolder}/plugins
src/main/java/com/gitblit/manager/GitblitManager.java
@@ -61,6 +61,8 @@
import com.gitblit.models.GitClientApplication;
import com.gitblit.models.Mailing;
import com.gitblit.models.Metric;
import com.gitblit.models.PluginRegistry.PluginRegistration;
import com.gitblit.models.PluginRegistry.PluginRelease;
import com.gitblit.models.ProjectModel;
import com.gitblit.models.RegistrantAccessPermission;
import com.gitblit.models.RepositoryModel;
@@ -1180,6 +1182,10 @@
        return repositoryManager.isIdle(repository);
    }
    /*
     * PLUGIN MANAGER
     */
    @Override
    public <T> List<T> getExtensions(Class<T> clazz) {
        return pluginManager.getExtensions(clazz);
@@ -1196,6 +1202,36 @@
    }
    @Override
    public boolean refreshRegistry() {
        return pluginManager.refreshRegistry();
    }
    @Override
    public boolean installPlugin(String url) {
        return pluginManager.installPlugin(url);
    }
    @Override
    public boolean installPlugin(PluginRelease pv) {
        return pluginManager.installPlugin(pv);
    }
    @Override
    public List<PluginRegistration> getRegisteredPlugins() {
        return pluginManager.getRegisteredPlugins();
    }
    @Override
    public PluginRegistration lookupPlugin(String idOrName) {
        return pluginManager.lookupPlugin(idOrName);
    }
    @Override
    public PluginRelease lookupRelease(String idOrName, String version) {
        return pluginManager.lookupRelease(idOrName, version);
    }
    @Override
    public List<PluginWrapper> getPlugins() {
        return pluginManager.getPlugins();
    }
src/main/java/com/gitblit/manager/IPluginManager.java
@@ -15,8 +15,13 @@
 */
package com.gitblit.manager;
import java.util.List;
import ro.fortsoft.pf4j.PluginManager;
import ro.fortsoft.pf4j.PluginWrapper;
import com.gitblit.models.PluginRegistry.PluginRegistration;
import com.gitblit.models.PluginRegistry.PluginRelease;
public interface IPluginManager extends IManager, PluginManager {
@@ -27,12 +32,51 @@
     * @return PluginWrapper that loaded the given class
     */
    PluginWrapper whichPlugin(Class<?> clazz);
    /**
     * Delete the plugin represented by {@link PluginWrapper}.
     *
     *
     * @param wrapper
     * @return true if successful
     */
    boolean deletePlugin(PluginWrapper wrapper);
    /**
     * Refresh the plugin registry.
     */
    boolean refreshRegistry();
    /**
     * Install the plugin from the specified url.
     */
    boolean installPlugin(String url);
    /**
     * Install the plugin.
     */
    boolean installPlugin(PluginRelease pr);
    /**
     * The list of all registered plugins.
     *
     * @return a list of registered plugins
     */
    List<PluginRegistration> getRegisteredPlugins();
    /**
     * Lookup a plugin registration from the plugin registries.
     *
     * @param idOrName
     * @return a plugin registration or null
     */
    PluginRegistration lookupPlugin(String idOrName);
    /**
     * Lookup a plugin release.
     *
     * @param idOrName
     * @param version (use null for the current version)
     * @return the identified plugin version or null
     */
    PluginRelease lookupRelease(String idOrName, String version);
}
src/main/java/com/gitblit/manager/PluginManager.java
@@ -15,30 +15,56 @@
 */
package com.gitblit.manager;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ro.fortsoft.pf4j.DefaultPluginManager;
import ro.fortsoft.pf4j.PluginVersion;
import ro.fortsoft.pf4j.PluginWrapper;
import com.gitblit.Keys;
import com.gitblit.models.PluginRegistry;
import com.gitblit.models.PluginRegistry.PluginRegistration;
import com.gitblit.models.PluginRegistry.PluginRelease;
import com.gitblit.utils.Base64;
import com.gitblit.utils.FileUtils;
import com.gitblit.utils.JsonUtils;
import com.gitblit.utils.StringUtils;
import com.google.common.io.Files;
import com.google.common.io.InputSupplier;
/**
 * The plugin manager maintains the lifecycle of plugins. It is exposed as
 * Dagger bean. The extension consumers supposed to retrieve plugin  manager
 * from the Dagger DI and retrieve extensions provided by active plugins.
 *
 *
 * @author David Ostrovsky
 *
 *
 */
public class PluginManager extends DefaultPluginManager implements IPluginManager {
    private final Logger logger = LoggerFactory.getLogger(getClass());
    private final IRuntimeManager runtimeManager;
    // timeout defaults of Maven 3.0.4 in seconds
    private int connectTimeout = 20;
    private int readTimeout = 12800;
    public PluginManager(IRuntimeManager runtimeManager) {
        super(runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins"));
@@ -60,13 +86,13 @@
        stopPlugins();
        return null;
    }
    @Override
    public boolean deletePlugin(PluginWrapper pw) {
        File folder = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins");
        File pluginFolder = new File(folder, pw.getPluginPath());
        File pluginZip = new File(folder, pw.getPluginPath() + ".zip");
        if (pluginFolder.exists()) {
            FileUtils.delete(pluginFolder);
        }
@@ -75,4 +101,218 @@
        }
        return true;
    }
    @Override
    public boolean refreshRegistry() {
        String dr = "http://gitblit.github.io/gitblit-registry/plugins.json";
        String url = runtimeManager.getSettings().getString(Keys.plugins.registry, dr);
        try {
            return download(url);
        } catch (Exception e) {
            logger.error(String.format("Failed to retrieve plugins.json from %s", url), e);
        }
        return false;
    }
    protected List<PluginRegistry> getRegistries() {
        List<PluginRegistry> list = new ArrayList<PluginRegistry>();
        File folder = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins");
        FileFilter jsonFilter = new FileFilter() {
            @Override
            public boolean accept(File file) {
                return !file.isDirectory() && file.getName().toLowerCase().endsWith(".json");
            }
        };
        File [] files = folder.listFiles(jsonFilter);
        if (files == null || files.length == 0) {
            // automatically retrieve the registry if we don't have a local copy
            refreshRegistry();
            files = folder.listFiles(jsonFilter);
        }
        if (files == null || files.length == 0) {
            return list;
        }
        for (File file : files) {
            PluginRegistry registry = null;
            try {
                String json = FileUtils.readContent(file, "\n");
                registry = JsonUtils.fromJsonString(json, PluginRegistry.class);
            } catch (Exception e) {
                logger.error("Failed to deserialize " + file, e);
            }
            if (registry != null) {
                list.add(registry);
            }
        }
        return list;
    }
    @Override
    public List<PluginRegistration> getRegisteredPlugins() {
        List<PluginRegistration> list = new ArrayList<PluginRegistration>();
        Map<String, PluginRegistration> map = new TreeMap<String, PluginRegistration>();
        for (PluginRegistry registry : getRegistries()) {
            List<PluginRegistration> registrations = registry.registrations;
            list.addAll(registrations);
            for (PluginRegistration reg : registrations) {
                reg.installedRelease = null;
                map.put(reg.id, reg);
            }
        }
        for (PluginWrapper pw : getPlugins()) {
            String id = pw.getDescriptor().getPluginId();
            PluginVersion pv = pw.getDescriptor().getVersion();
            PluginRegistration reg = map.get(id);
            if (reg != null) {
                reg.installedRelease = pv.toString();
            }
        }
        return list;
    }
    @Override
    public PluginRegistration lookupPlugin(String idOrName) {
        for (PluginRegistry registry : getRegistries()) {
            PluginRegistration reg = registry.lookup(idOrName);
            if (reg != null) {
                return reg;
            }
        }
        return null;
    }
    @Override
    public PluginRelease lookupRelease(String idOrName, String version) {
        for (PluginRegistry registry : getRegistries()) {
            PluginRegistration reg = registry.lookup(idOrName);
            if (reg != null) {
                PluginRelease pv;
                if (StringUtils.isEmpty(version)) {
                    pv = reg.getCurrentRelease();
                } else {
                    pv = reg.getRelease(version);
                }
                if (pv != null) {
                    return pv;
                }
            }
        }
        return null;
    }
    /**
     * Installs the plugin from the plugin version.
     *
     * @param pv
     * @throws IOException
     * @return true if successful
     */
    @Override
    public boolean installPlugin(PluginRelease pv) {
        return installPlugin(pv.url);
    }
    /**
     * Installs the plugin from the url.
     *
     * @param url
     * @return true if successful
     */
    @Override
    public boolean installPlugin(String url) {
        try {
            if (!download(url)) {
                return false;
            }
            // TODO stop, unload, load
        } catch (IOException e) {
            logger.error("Failed to install plugin from " + url, e);
        }
        return true;
    }
    /**
     * Download a file to the plugins folder.
     *
     * @param url
     * @return
     * @throws IOException
     */
    protected boolean download(String url) throws IOException {
        File pFolder = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins");
        File tmpFile = new File(pFolder, StringUtils.getSHA1(url) + ".tmp");
        if (tmpFile.exists()) {
            tmpFile.delete();
        }
        URL u = new URL(url);
        final URLConnection conn = getConnection(u);
        // try to get the server-specified last-modified date of this artifact
        long lastModified = conn.getHeaderFieldDate("Last-Modified", System.currentTimeMillis());
        Files.copy(new InputSupplier<InputStream>() {
             @Override
            public InputStream getInput() throws IOException {
                 return new BufferedInputStream(conn.getInputStream());
            }
        }, tmpFile);
        File destFile = new File(pFolder, StringUtils.getLastPathElement(u.getPath()));
        if (destFile.exists()) {
            destFile.delete();
        }
        tmpFile.renameTo(destFile);
        destFile.setLastModified(lastModified);
        return true;
    }
    protected URLConnection getConnection(URL url) throws IOException {
        java.net.Proxy proxy = getProxy(url);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection(proxy);
        if (java.net.Proxy.Type.DIRECT != proxy.type()) {
            String auth = getProxyAuthorization(url);
            conn.setRequestProperty("Proxy-Authorization", auth);
        }
        String username = null;
        String password = null;
        if (!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)) {
            // set basic authentication header
            String auth = Base64.encodeBytes((username + ":" + password).getBytes());
            conn.setRequestProperty("Authorization", "Basic " + auth);
        }
        // configure timeouts
        conn.setConnectTimeout(connectTimeout * 1000);
        conn.setReadTimeout(readTimeout * 1000);
        switch (conn.getResponseCode()) {
        case HttpURLConnection.HTTP_MOVED_TEMP:
        case HttpURLConnection.HTTP_MOVED_PERM:
            // handle redirects by closing this connection and opening a new
            // one to the new location of the requested resource
            String newLocation = conn.getHeaderField("Location");
            if (!StringUtils.isEmpty(newLocation)) {
                logger.info("following redirect to {0}", newLocation);
                conn.disconnect();
                return getConnection(new URL(newLocation));
            }
        }
        return conn;
    }
    protected Proxy getProxy(URL url) {
        return java.net.Proxy.NO_PROXY;
    }
    protected String getProxyAuthorization(URL url) {
        return "";
    }
}
src/main/java/com/gitblit/models/PluginRegistry.java
New file
@@ -0,0 +1,143 @@
/*
 * Copyright 2014 gitblit.com.
 *
 * 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.gitblit.models;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.parboiled.common.StringUtils;
import ro.fortsoft.pf4j.PluginVersion;
/**
 * Represents a list of plugin registrations.
 */
public class PluginRegistry implements Serializable {
    private static final long serialVersionUID = 1L;
    public final String name;
    public final List<PluginRegistration> registrations;
    public PluginRegistry(String name) {
        this.name = name;
        registrations = new ArrayList<PluginRegistration>();
    }
    public PluginRegistration lookup(String idOrName) {
        for (PluginRegistration registration : registrations) {
            if (registration.id.equalsIgnoreCase(idOrName)
                    || registration.name.equalsIgnoreCase(idOrName)) {
                return registration;
            }
        }
        return null;
    }
    @Override
    public String toString() {
        return getClass().getSimpleName();
    }
    public static enum InstallState {
        NOT_INSTALLED, INSTALLED, CAN_UPDATE, UNKNOWN
    }
    /**
     * Represents a plugin registration.
     */
    public static class PluginRegistration implements Serializable {
        private static final long serialVersionUID = 1L;
        public final String id;
        public String name;
        public String description;
        public String provider;
        public String projectUrl;
        public String currentRelease;
        public transient String installedRelease;
        public List<PluginRelease> releases;
        public PluginRegistration(String id) {
            this.id = id;
            this.releases = new ArrayList<PluginRelease>();
        }
        public PluginRelease getCurrentRelease() {
            PluginRelease current = null;
            if (!StringUtils.isEmpty(currentRelease)) {
                current = getRelease(currentRelease);
            }
            if (current == null) {
                Date date = new Date(0);
                for (PluginRelease pv : releases) {
                    if (pv.date.after(date)) {
                        current = pv;
                    }
                }
            }
            return current;
        }
        public PluginRelease getRelease(String version) {
            for (PluginRelease pv : releases) {
                if (pv.version.equalsIgnoreCase(version)) {
                    return pv;
                }
            }
            return null;
        }
        public InstallState getInstallState() {
            if (StringUtils.isEmpty(installedRelease)) {
                return InstallState.NOT_INSTALLED;
            }
            PluginVersion ir = PluginVersion.createVersion(installedRelease);
            PluginVersion cr = PluginVersion.createVersion(currentRelease);
            switch (ir.compareTo(cr)) {
            case -1:
                return InstallState.UNKNOWN;
            case 1:
                return InstallState.CAN_UPDATE;
            default:
                return InstallState.INSTALLED;
            }
        }
        @Override
        public String toString() {
            return id;
        }
    }
    public static class PluginRelease {
        public String version;
        public Date date;
        public String url;
    }
}
src/main/java/com/gitblit/transport/ssh/commands/PluginDispatcher.java
@@ -19,6 +19,7 @@
import java.util.List;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
import ro.fortsoft.pf4j.PluginDependency;
import ro.fortsoft.pf4j.PluginDescriptor;
@@ -26,6 +27,8 @@
import ro.fortsoft.pf4j.PluginWrapper;
import com.gitblit.manager.IGitblit;
import com.gitblit.models.PluginRegistry.PluginRegistration;
import com.gitblit.models.PluginRegistry.PluginRelease;
import com.gitblit.models.UserModel;
import com.gitblit.utils.FlipTable;
import com.gitblit.utils.FlipTable.Borders;
@@ -46,7 +49,8 @@
        register(user, StopPlugin.class);
        register(user, ShowPlugin.class);
        register(user, RemovePlugin.class);
        register(user, UploadPlugin.class);
        register(user, InstallPlugin.class);
        register(user, AvailablePlugins.class);
    }
    @CommandMetaData(name = "list", aliases = { "ls" }, description = "List the loaded plugins")
@@ -82,7 +86,7 @@
            stdout.println(FlipTable.of(headers, data, Borders.BODY_HCOLS));
        }
        @Override
        protected void asTabbed(List<PluginWrapper> list) {
            for (PluginWrapper pw : list) {
@@ -95,7 +99,7 @@
            }
        }
    }
    @CommandMetaData(name = "start", description = "Start a plugin")
    public static class StartPlugin extends SshCommand {
@@ -128,7 +132,7 @@
                }
            }
        }
        protected void start(PluginWrapper pw) throws UnloggedFailure {
            String id = pw.getDescriptor().getPluginId();
            if (pw.getPluginState() == PluginState.STARTED) {
@@ -143,7 +147,7 @@
            }
        }
    }
    @CommandMetaData(name = "stop", description = "Stop a plugin")
    public static class StopPlugin extends SshCommand {
@@ -177,7 +181,7 @@
            }
            }
        }
        protected void stop(PluginWrapper pw) throws UnloggedFailure {
            String id = pw.getDescriptor().getPluginId();
            if (pw.getPluginState() == PluginState.STOPPED) {
@@ -192,7 +196,7 @@
            }
        }
    }
    @CommandMetaData(name = "show", description = "Show the details of a plugin")
    public static class ShowPlugin extends SshCommand {
@@ -230,7 +234,7 @@
                    String ext = exts.get(i);
                    data[0] = new Object[] { ext.toString(), ext.toString() };
                }
                extensions = FlipTable.of(headers, data, Borders.COLS);
                extensions = FlipTable.of(headers, data, Borders.COLS);
            }
            // DEPENDENCIES
@@ -246,9 +250,9 @@
                    PluginDependency dep = deps.get(i);
                    data[0] = new Object[] { dep.getPluginId(), dep.getPluginVersion() };
                }
                dependencies = FlipTable.of(headers, data, Borders.COLS);
                dependencies = FlipTable.of(headers, data, Borders.COLS);
            }
            String[] headers = { d.getPluginId() };
            Object[][] data = new Object[5][];
            data[0] = new Object[] { fields };
@@ -256,10 +260,10 @@
            data[2] = new Object[] { extensions };
            data[3] = new Object[] { "DEPENDENCIES" };
            data[4] = new Object[] { dependencies };
            stdout.println(FlipTable.of(headers, data));
            stdout.println(FlipTable.of(headers, data));
        }
    }
    @CommandMetaData(name = "remove", aliases= { "rm", "del" }, description = "Remove a plugin", hidden = true)
    public static class RemovePlugin extends SshCommand {
@@ -282,12 +286,98 @@
            }
        }
    }
    @CommandMetaData(name = "receive", aliases= { "upload" }, description = "Upload a plugin to the server", hidden = true)
    public static class UploadPlugin extends SshCommand {
    @CommandMetaData(name = "install", description = "Download and installs a plugin", hidden = true)
    public static class InstallPlugin extends SshCommand {
        @Argument(index = 0, required = true, metaVar = "<URL>|<ID>|<NAME>", usage = "the id, name, or the url of the plugin to download and install")
        protected String urlOrIdOrName;
        @Option(name = "--version", usage = "The specific version to install")
        private String version;
        @Override
        public void run() throws UnloggedFailure {
            IGitblit gitblit = getContext().getGitblit();
            try {
                String ulc = urlOrIdOrName.toLowerCase();
                if (ulc.startsWith("http://") || ulc.startsWith("https://")) {
                    if (gitblit.installPlugin(urlOrIdOrName)) {
                        stdout.println(String.format("Installed %s", urlOrIdOrName));
                    } else {
                        new UnloggedFailure(1, String.format("Failed to install %s", urlOrIdOrName));
                    }
                } else {
                    PluginRelease pv = gitblit.lookupRelease(urlOrIdOrName, version);
                    if (pv == null) {
                        throw new UnloggedFailure(1,  String.format("Plugin \"%s\" is not in the registry!", urlOrIdOrName));
                    }
                    if (gitblit.installPlugin(pv)) {
                        stdout.println(String.format("Installed %s", urlOrIdOrName));
                    } else {
                        throw new UnloggedFailure(1, String.format("Failed to install %s", urlOrIdOrName));
                    }
                }
            } catch (Exception e) {
                log.error("Failed to install " + urlOrIdOrName, e);
                throw new UnloggedFailure(1, String.format("Failed to install %s", urlOrIdOrName), e);
            }
        }
    }
    @CommandMetaData(name = "available", description = "List the available plugins")
    public static class AvailablePlugins extends ListFilterCommand<PluginRegistration> {
        @Option(name = "--refresh", aliases = { "-r" }, usage = "refresh the plugin registry")
        protected boolean refresh;
        @Override
        protected List<PluginRegistration> getItems() throws UnloggedFailure {
            IGitblit gitblit = getContext().getGitblit();
            if (refresh) {
                gitblit.refreshRegistry();
            }
            List<PluginRegistration> list = gitblit.getRegisteredPlugins();
            return list;
        }
        @Override
        protected boolean matches(String filter, PluginRegistration t) {
            return t.id.matches(filter) || t.name.matches(filter);
        }
        @Override
        protected void asTable(List<PluginRegistration> list) {
            String[] headers;
            if (verbose) {
                String [] h = { "Name", "Description", "Installed", "Release", "State", "Id", "Provider" };
                headers = h;
            } else {
                String [] h = { "Name", "Description", "Installed", "Release", "State" };
                headers = h;
            }
            Object[][] data = new Object[list.size()][];
            for (int i = 0; i < list.size(); i++) {
                PluginRegistration p = list.get(i);
                if (verbose) {
                    data[i] = new Object[] {p.name, p.description, p.installedRelease, p.currentRelease, p.getInstallState(), p.id, p.provider};
                } else {
                    data[i] = new Object[] {p.name, p.description, p.installedRelease, p.currentRelease, p.getInstallState()};
                }
            }
            stdout.println(FlipTable.of(headers, data, Borders.BODY_HCOLS));
        }
        @Override
        protected void asTabbed(List<PluginRegistration> list) {
            for (PluginRegistration p : list) {
                if (verbose) {
                    outTabbed(p.name, p.description, p.currentRelease, p.getInstallState(), p.id, p.provider);
                } else {
                    outTabbed(p.name, p.description, p.currentRelease, p.getInstallState());
                }
            }
        }
    }
}