package restx.plugins;
import com.google.common.base.*;
import com.google.common.collect.ImmutableList;
import com.google.common.io.ByteStreams;
import com.google.common.io.CharStreams;
import com.google.common.io.Files;
import com.google.common.io.PatternFilenameFilter;
import com.google.common.io.Resources;
import jline.console.completer.ArgumentCompleter;
import jline.console.completer.Completer;
import jline.console.completer.StringsCompleter;
import org.apache.ivy.core.module.id.ModuleRevisionId;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import restx.factory.Component;
import restx.shell.RestxShell;
import restx.shell.ShellCommandRunner;
import restx.shell.ShellIvy;
import restx.shell.StdShellCommand;
import java.io.*;
import java.net.URL;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* User: xavierhanin
* Date: 5/4/13
* Time: 2:32 PM
*/
@Component
public class PluginsShellCommand extends StdShellCommand {
private static final Logger logger = LoggerFactory.getLogger(PluginsShellCommand.class);
/*
* this is the default exlcusions list used when fetching plugins (to avoid confusion between lib and plugins dir):
* - we exclude the main modules already part of the shell itself (shell and shell-manager)
* - we exclude logback-classic, to avoid a SLF4J warning at startup if multiple bindings are present
*/
private static final List<String> defaultExcludes = ImmutableList.of(
"io.restx:restx-shell",
"io.restx:restx-shell-manager",
"ch.qos.logback:logback-classic");
private static final ModulesManager.DownloadOptions defaultDownloadOptions = new ModulesManager.DownloadOptions.Builder().exclusions(defaultExcludes).build();
public PluginsShellCommand() {
super(ImmutableList.of("shell"), "manages the shell itself: install / update plugins, upgrade restx shell version");
}
@Override
protected String resourceMan() {
return "restx/plugins/shell.man";
}
@Override
protected Optional<? extends ShellCommandRunner> doMatch(String line) {
final List<String> args = splitArgs(line);
if (args.size() < 2) {
return Optional.absent();
}
switch (args.get(1)) {
case "install":
return Optional.<ShellCommandRunner>of(new InstallPluginRunner(args));
case "upgrade":
return Optional.<ShellCommandRunner>of(new UpgradeShellRunner());
}
return Optional.absent();
}
@Override
public Iterable<Completer> getCompleters() {
return ImmutableList.<Completer>of(
new ArgumentCompleter(new StringsCompleter("shell"), new StringsCompleter("install", "upgrade")));
}
@Override
public void start(RestxShell shell) throws IOException {
File versionFile = new File(shell.installLocation().toFile(), shell.version());
if (versionFile.exists()) {
// upgrade check already done
return;
}
try {
// upgrading to a new version, we check if plugins need to be upgraded
File[] pluginFiles = pluginFiles(pluginsDir(shell));
if (pluginFiles.length == 0) {
// no plugin installed, nothing to upgrade
return;
}
shell.printIn("upgrading to " + shell.version() + " ...", RestxShell.AnsiCodes.ANSI_YELLOW);
shell.println("");
ModulesManager modulesManager = new ModulesManager(
new URL("http://restx.io/modules"), ShellIvy.loadIvy(shell));
List<ModuleDescriptor> plugins = modulesManager.searchModules("category=shell");
Set<String> allJars = new LinkedHashSet<>();
Set<String> keepJars = new LinkedHashSet<>();
List<ModuleDescriptor> pluginsToInstall = new ArrayList<>();
List<String> unmatchedPlugins = new ArrayList<>();
for (File pluginFile : pluginFiles) {
try {
List<String> desc = Files.readLines(pluginFile, Charsets.UTF_8);
String id = desc.get(0);
ModuleRevisionId mrid = ModulesManager.toMrid(id);
List<String> jars = desc.subList(2, desc.size());
allJars.addAll(jars);
ModuleDescriptor matchingModule = findMatchingPlugin(plugins, mrid);
if (matchingModule == null) {
keepJars.addAll(jars);
unmatchedPlugins.add(id);
} else if (ModulesManager.toMrid(matchingModule.getId()).getRevision()
.equals(mrid.getRevision())) {
// up to date
keepJars.addAll(jars);
} else {
pluginsToInstall.add(matchingModule);
}
} catch (Exception e) {
shell.printIn("error while parsing plugin file " + pluginFile + ": " + e, RestxShell.AnsiCodes.ANSI_RED);
shell.println("");
}
}
if (!unmatchedPlugins.isEmpty()) {
shell.printIn("found unmanaged installed plugins, they won't be upgraded automatically:\n"
+ Joiner.on("\n").join(unmatchedPlugins), RestxShell.AnsiCodes.ANSI_YELLOW);
shell.println("");
}
Set<String> jarsToRemove = new LinkedHashSet<>();
jarsToRemove.addAll(allJars);
jarsToRemove.removeAll(keepJars);
for (String jarToRemove : jarsToRemove) {
logger.debug("removing {}", jarToRemove);
new File(jarToRemove).delete();
}
if (!pluginsToInstall.isEmpty()) {
int count = 0;
shell.println("found " + pluginsToInstall.size() + " plugins to upgrade");
for (ModuleDescriptor md : pluginsToInstall) {
if (installPlugin(shell, modulesManager, pluginsDir(shell), md)) {
count++;
}
}
if (count > 0) {
shell.println("upgraded " + count + " plugins, restarting shell");
shell.restart();
}
}
} finally {
Files.write(DateTime.now().toString(), versionFile, Charsets.UTF_8);
}
}
private ModuleDescriptor findMatchingPlugin(List<ModuleDescriptor> plugins, ModuleRevisionId mrid) {
ModuleDescriptor matchingModule = null;
for (ModuleDescriptor plugin : plugins) {
ModuleRevisionId pluginId = ModulesManager.toMrid(plugin.getId());
if (pluginId.getModuleId().equals(mrid.getModuleId())) {
matchingModule = plugin;
break;
}
}
return matchingModule;
}
private class InstallPluginRunner implements ShellCommandRunner {
private final Optional<List<String>> pluginIds;
public InstallPluginRunner(List<String> args) {
if (args.size() > 2) {
pluginIds = Optional.<List<String>>of(new ArrayList<>(args.subList(2, args.size())));
} else {
pluginIds = Optional.absent();
}
}
@Override
public void run(RestxShell shell) throws Exception {
ModulesManager modulesManager = new ModulesManager(
new URL("http://restx.io/modules"), ShellIvy.loadIvy(shell));
shell.println("looking for plugins...");
List<ModuleDescriptor> plugins = modulesManager.searchModules("category=shell");
Iterable<String> selected = null;
if (!pluginIds.isPresent()) {
shell.printIn("found " + plugins.size() + " available plugins", RestxShell.AnsiCodes.ANSI_CYAN);
shell.println("");
for (int i = 0; i < plugins.size(); i++) {
ModuleDescriptor plugin = plugins.get(i);
shell.printIn(String.format(" [%3d] %s%n", i + 1, plugin.getId()), RestxShell.AnsiCodes.ANSI_PURPLE);
shell.println("\t\t" + plugin.getDescription());
}
String sel = shell.ask("Which plugin would you like to install (eg '1 3 5')? \nYou can also provide a plugin id in the form <groupId>:<moduleId>:<version>\n plugin to install: ", "");
selected = Splitter.on(" ").trimResults().omitEmptyStrings().split(sel);
} else {
selected = pluginIds.get();
}
File pluginsDir = pluginsDir(shell);
int count = 0;
for (String s : selected) {
ModuleDescriptor md;
if (CharMatcher.DIGIT.matchesAllOf(s)) {
int i = Integer.parseInt(s);
md = plugins.get(i - 1);
} else {
md = new ModuleDescriptor(s, "shell", "");
}
if (installPlugin(shell, modulesManager, pluginsDir, md)) {
count++;
}
}
if (count > 0) {
shell.printIn("installed " + count + " plugins, restarting shell to take them into account", RestxShell.AnsiCodes.ANSI_GREEN);
shell.println("");
shell.restart();
} else {
shell.println("no plugin installed");
}
}
}
private boolean installPlugin(RestxShell shell, ModulesManager modulesManager, File pluginsDir, ModuleDescriptor md) throws IOException {
shell.printIn("installing " + md.getId() + "...", RestxShell.AnsiCodes.ANSI_CYAN);
shell.println("");
try {
List<File> copied = modulesManager.download(ImmutableList.of(md), pluginsDir, defaultDownloadOptions);
if (!copied.isEmpty()) {
shell.printIn("installed " + md.getId(), RestxShell.AnsiCodes.ANSI_GREEN);
shell.println("");
Files.write(md.getId() + "\n"
+ DateTime.now() + "\n"
+ Joiner.on("\n").join(copied),
pluginFile(pluginsDir, md), Charsets.UTF_8);
return true;
} else {
shell.printIn("problem while installing " + md.getId(), RestxShell.AnsiCodes.ANSI_RED);
shell.println("");
}
} catch (IOException e) {
shell.printIn("IO problem while installing " + md.getId() + "\n" + e.getMessage(), RestxShell.AnsiCodes.ANSI_RED);
shell.println("");
} catch (IllegalStateException e) {
shell.printIn(e.getMessage(), RestxShell.AnsiCodes.ANSI_RED);
shell.println("");
}
return false;
}
private File pluginsDir(RestxShell shell) {
return new File(shell.installLocation().toFile(), "plugins");
}
private File pluginFile(File pluginsDir, ModuleDescriptor md) {
return new File(pluginsDir, md.getModuleId() + ".plugin");
}
private File[] pluginFiles(File pluginsDir) {
File[] files = pluginsDir.listFiles(new PatternFilenameFilter(".*\\.plugin"));
return files == null ? new File[0] : files;
}
private class UpgradeShellRunner implements ShellCommandRunner {
@Override
public void run(RestxShell shell) throws Exception {
shell.println("checking for upgrade of restx shell...");
try (Reader reader = new InputStreamReader(new URL("http://restx.io/version").openStream(), Charsets.UTF_8)) {
List<String> parts = CharStreams.readLines(reader);
if (parts.size() < 2) {
shell.printIn(
"unexpected content at http://restx.io/version, try again later or contact the group.\n",
RestxShell.AnsiCodes.ANSI_RED);
shell.println("content: ");
shell.println(Joiner.on("\n").join(parts));
return;
}
String version = parts.get(0);
String url = parts.get(1);
if (!version.equals(shell.version())) {
shell.printIn("upgrading to " + version, RestxShell.AnsiCodes.ANSI_GREEN);
shell.println("");
shell.println("please wait while downloading new version, this may take a while...");
boolean isWindows = System.getProperty("os.name").toLowerCase().indexOf("win") >= 0;
String archiveExt = isWindows ? ".zip" : ".tar.gz";
String scriptExt = isWindows ? ".bat" : ".sh";
URL source = new URL(url + archiveExt);
shell.download(source, shell.installLocation().resolve("upgrade" + archiveExt).toFile());
Resources.asByteSource(Resources.getResource(PluginsShellCommand.class, "upgrade" + scriptExt))
.copyTo(Files.asByteSink(shell.installLocation().resolve("upgrade" + scriptExt).toFile()));
shell.println("downloaded version " + version + ", restarting");
shell.restart();
}
}
}
}
}