package org.rakam.plugin; import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterators; import org.rakam.ui.util.HttpDownloadHelper; import java.io.File; import java.io.IOException; import java.io.PrintStream; import java.net.MalformedURLException; import java.net.URL; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; public class PluginManager { private static final File pluginsDirectory = new File(new File(System.getProperty("user.dir")), "plugins"); private static final int EXIT_CODE_OK = 0; private static final int EXIT_CODE_CMD_USAGE = 64; private static final int EXIT_CODE_IO_ERROR = 74; private static final int EXIT_CODE_ERROR = 70; public static final class ACTION { public static final int NONE = 0; public static final int INSTALL = 1; public static final int REMOVE = 2; public static final int LIST = 3; } public enum OutputMode { DEFAULT, SILENT, VERBOSE } private static final ImmutableSet<Object> BLACKLIST = ImmutableSet.builder() .add("rakam", "rakam.bat", "rakam.in.sh", "plugin", "plugin.bat", "service.bat").build(); private String url; private OutputMode outputMode; public PluginManager(String url, OutputMode outputMode) { this.url = url; this.outputMode = outputMode; } public void downloadAndExtract(String name) throws IOException { if (name == null) { throw new IllegalArgumentException("plugin name must be supplied with --install [name]."); } HttpDownloadHelper downloadHelper = new HttpDownloadHelper(); boolean downloaded = false; HttpDownloadHelper.DownloadProgress progress; if (outputMode == OutputMode.SILENT) { progress = new HttpDownloadHelper.NullProgress(); } else { progress = new HttpDownloadHelper.VerboseProgress(SysOut.getOut()); } if (!Files.isWritable(pluginsDirectory.toPath())) { throw new IOException("plugin directory " + pluginsDirectory.getAbsolutePath() + " is read only"); } PluginHandle pluginHandle = PluginHandle.parse(name); // checkForForbiddenName(pluginHandle.name); // Path pluginFile = pluginHandle.distroFile(environment); // final Path extractLocation = pluginHandle.extractedDir(environment); // if (Files.exists(extractLocation)) { // throw new IOException("plugin directory " + extractLocation.toAbsolutePath() + " already exists. To update the plugin, uninstall it first using --remove " + name + " command"); // } // first, try directly from the URL provided Path pluginFile = null; if (url != null) { URL pluginUrl = new URL(url); log("Trying " + pluginUrl.toExternalForm() + "..."); try { downloadHelper.download(pluginUrl, pluginFile, progress); downloaded = true; } catch (Exception e) { // ignore } } if (!downloaded) { // We try all possible locations for (URL url : pluginHandle.urls()) { log("Trying " + url.toExternalForm() + "..."); try { downloadHelper.download(url, pluginFile, progress); downloaded = true; break; } catch (Exception e) { } } } if (!downloaded) { throw new IOException("failed to download out of all possible locations..., use --verbose to get detailed information"); } } // public void removePlugin(String name) throws IOException { // if (name == null) { // throw new IllegalArgumentException("plugin name must be supplied with --remove [name]."); // } // PluginHandle pluginHandle = PluginHandle.parse(name); // boolean removed = false; // // checkForForbiddenName(pluginHandle.name); // Path pluginToDelete = pluginHandle.extractedDir(environment); // if (Files.exists(pluginToDelete)) { // debug("Removing: " + pluginToDelete); // try { // IOUtils.rm(pluginToDelete); // } catch (IOException ex){ // throw new IOException("Unable to remove " + pluginHandle.name + ". Check file permissions on " + // pluginToDelete.toString(), ex); // } // removed = true; // } // pluginToDelete = pluginHandle.distroFile(environment); // if (Files.exists(pluginToDelete)) { // debug("Removing: " + pluginToDelete); // try { // Files.delete(pluginToDelete); // } catch (Exception ex) { // throw new IOException("Unable to remove " + pluginHandle.name + ". Check file permissions on " + // pluginToDelete.toString(), ex); // } // removed = true; // } // Path binLocation = pluginHandle.binDir(environment); // if (Files.exists(binLocation)) { // debug("Removing: " + binLocation); // try { // IOUtils.rm(binLocation); // } catch (IOException ex){ // throw new IOException("Unable to remove " + pluginHandle.name + ". Check file permissions on " + // binLocation.toString(), ex); // } // removed = true; // } // // if (removed) { // log("Removed " + name); // } else { // log("Plugin " + name + " not found. Run plugin --list to get list of installed plugins."); // } // } // // private static void checkForForbiddenName(String name) { // if (!hasLength(name) || BLACKLIST.contains(name.toLowerCase(Locale.ROOT))) { // throw new IllegalArgumentException("Illegal plugin name: " + name); // } // } public Path[] getListInstalledPlugins() throws IOException { try (DirectoryStream<Path> stream = Files.newDirectoryStream(pluginsDirectory.toPath())) { return Iterators.toArray(stream.iterator(), Path.class); } } public void listInstalledPlugins() throws IOException { Path[] plugins = getListInstalledPlugins(); log("Installed plugins in " + pluginsDirectory.toPath().toAbsolutePath() + ":"); if (plugins == null || plugins.length == 0) { log(" - No plugin detected"); } else { for (int i = 0; i < plugins.length; i++) { log(" - " + plugins[i].getFileName()); } } } public static void main(String[] args) { try { Files.createDirectories(pluginsDirectory.toPath()); } catch (IOException e) { displayHelp("Unable to create plugins dir: "); System.exit(EXIT_CODE_ERROR); } String url = null; OutputMode outputMode = OutputMode.DEFAULT; String pluginName = null; int action = ACTION.NONE; if (args.length < 1) { displayHelp(null); } try { for (int c = 0; c < args.length; c++) { String command = args[c]; switch (command) { case "-u": case "--url": // deprecated versions: case "url": case "-url": url = getCommandValue(args, ++c, "--url"); // Until update is supported, then supplying a URL implies installing // By specifying this action, we also avoid silently failing without // dubious checks. action = ACTION.INSTALL; break; case "-v": case "--verbose": // deprecated versions: case "verbose": case "-verbose": outputMode = OutputMode.VERBOSE; break; case "-s": case "--silent": // deprecated versions: case "silent": case "-silent": outputMode = OutputMode.SILENT; break; case "-i": case "--install": // deprecated versions: case "install": case "-install": pluginName = getCommandValue(args, ++c, "--install"); action = ACTION.INSTALL; break; case "-r": case "--remove": // deprecated versions: case "remove": case "-remove": pluginName = getCommandValue(args, ++c, "--remove"); action = ACTION.REMOVE; break; case "-l": case "--list": action = ACTION.LIST; break; case "-h": case "--help": displayHelp(null); break; default: displayHelp("Command [" + command + "] unknown."); // Unknown command. We break... System.exit(EXIT_CODE_CMD_USAGE); } } } catch (Throwable e) { displayHelp("Error while parsing options: " + e.getClass().getSimpleName() + ": " + e.getMessage()); System.exit(EXIT_CODE_CMD_USAGE); } if (action > ACTION.NONE) { int exitCode = EXIT_CODE_ERROR; // we fail unless it's reset PluginManager pluginManager = new PluginManager(url, outputMode); switch (action) { case ACTION.INSTALL: try { pluginManager.log("-> Installing " + Strings.nullToEmpty(pluginName) + "..."); pluginManager.downloadAndExtract(pluginName); exitCode = EXIT_CODE_OK; } catch (IOException e) { exitCode = EXIT_CODE_IO_ERROR; pluginManager.log("Failed to install " + pluginName + ", reason: " + e.getMessage()); } catch (Throwable e) { exitCode = EXIT_CODE_ERROR; displayHelp("Error while installing plugin, reason: " + e.getClass().getSimpleName() + ": " + e.getMessage()); } break; case ACTION.REMOVE: try { pluginManager.log("-> Removing " + Strings.nullToEmpty(pluginName) + "..."); // pluginManager.removePlugin(pluginName); exitCode = EXIT_CODE_OK; } catch (IllegalArgumentException e) { exitCode = EXIT_CODE_CMD_USAGE; pluginManager.log("Failed to remove " + pluginName + ", reason: " + e.getMessage()); // } catch (IOException e) { // exitCode = EXIT_CODE_IO_ERROR; // pluginManager.log("Failed to remove " + pluginName + ", reason: " + e.getMessage()); } catch (Throwable e) { exitCode = EXIT_CODE_ERROR; displayHelp("Error while removing plugin, reason: " + e.getClass().getSimpleName() + ": " + e.getMessage()); } break; case ACTION.LIST: try { pluginManager.listInstalledPlugins(); exitCode = EXIT_CODE_OK; } catch (Throwable e) { displayHelp("Error while listing plugins, reason: " + e.getClass().getSimpleName() + ": " + e.getMessage()); } break; default: pluginManager.log("Unknown Action [" + action + "]"); exitCode = EXIT_CODE_ERROR; } System.exit(exitCode); // exit here! } } /** * Get the value for the {@code flag} at the specified {@code arg} of the command line {@code args}. * <p /> * This is useful to avoid having to check for multiple forms of unset (e.g., " " versus "" versus {@code null}). * @param args Incoming command line arguments. * @param arg Expected argument containing the value. * @param flag The flag whose value is being retrieved. * @return Never {@code null}. The trimmed value. * @throws NullPointerException if {@code args} is {@code null}. * @throws ArrayIndexOutOfBoundsException if {@code arg} is negative. * @throws IllegalStateException if {@code arg} is >= {@code args.length}. * @throws IllegalArgumentException if the value evaluates to blank ({@code null} or only whitespace) */ private static String getCommandValue(String[] args, int arg, String flag) { if (arg >= args.length) { throw new IllegalStateException("missing value for " + flag + ". Usage: " + flag + " [value]"); } // avoid having to interpret multiple forms of unset String trimmedValue = Strings.emptyToNull(args[arg].trim()); // If we had a value that is blank, then fail immediately if (trimmedValue == null) { throw new IllegalArgumentException( "value for " + flag + "('" + args[arg] + "') must be set. Usage: " + flag + " [value]"); } return trimmedValue; } private static void displayHelp(String message) { SysOut.println("Usage:"); SysOut.println(" -u, --url [plugin location] : Set exact URL to download the plugin from"); SysOut.println(" -i, --install [plugin name] : Downloads and installs listed plugins [*]"); SysOut.println(" -t, --timeout [duration] : Timeout setting: 30s, 1m, 1h... (infinite by default)"); SysOut.println(" -r, --remove [plugin name] : Removes listed plugins"); SysOut.println(" -l, --list : List installed plugins"); SysOut.println(" -v, --verbose : Prints verbose messages"); SysOut.println(" -s, --silent : Run in silent mode"); SysOut.println(" -h, --help : Prints this help message"); SysOut.newline(); SysOut.println(" [*] Plugin name could be:"); SysOut.println(" groupId/artifactId/version for community plugins (download from maven central or oss sonatype)"); SysOut.println(" username/repository for site plugins (download from github master)"); if (message != null) { SysOut.newline(); SysOut.println("Message:"); SysOut.println(" " + message); } } private void debug(String line) { if (outputMode == OutputMode.VERBOSE) SysOut.println(line); } private void log(String line) { if (outputMode != OutputMode.SILENT) SysOut.println(line); } static class SysOut { public static void newline() { System.out.println(); } public static void println(String msg) { System.out.println(msg); } public static PrintStream getOut() { return System.out; } } /** * Helper class to extract properly user name, repository name, version and plugin name * from plugin name given by a user. */ static class PluginHandle { private final String name; private final String version; private final String user; private final String repo; PluginHandle(String name, String version, String user, String repo) { this.name = name; this.version = version; this.user = user; this.repo = repo; } List<URL> urls() { List<URL> urls = new ArrayList<>(); if (version != null) { // Maven central repository addUrl(urls, "http://search.maven.org/remotecontent?filepath=" + user.replace('.', '/') + "/" + repo + "/" + version + "/" + repo + "-" + version + ".zip"); // Sonatype repository addUrl(urls, "https://oss.sonatype.org/service/local/repositories/releases/content/" + user.replace('.', '/') + "/" + repo + "/" + version + "/" + repo + "-" + version + ".zip"); // Github repository addUrl(urls, "https://github.com/" + user + "/" + repo + "/archive/" + version + ".zip"); } // Github repository for master branch (assume site) addUrl(urls, "https://github.com/" + user + "/" + repo + "/archive/master.zip"); return urls; } private static void addUrl(List<URL> urls, String url) { try { urls.add(new URL(url)); } catch (MalformedURLException e) { // We simply ignore malformed URL } } static PluginHandle parse(String name) { String[] elements = name.split("/"); // We first consider the simplest form: pluginname String repo = elements[0]; String user = null; String version = null; // We consider the form: username/pluginname if (elements.length > 1) { user = elements[0]; repo = elements[1]; // We consider the form: username/pluginname/version if (elements.length > 2) { version = elements[2]; } } if (repo.startsWith("rakam-")) { // remove rakam- prefix String endname = repo.substring("rakam-".length()); return new PluginHandle(endname, version, user, repo); } return new PluginHandle(repo, version, user, repo); } } }