/* * Copyright 2010-2013, CloudBees Inc. * * 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.cloudbees.sdk; import com.cloudbees.sdk.cli.*; import com.cloudbees.sdk.extensibility.AnnotationLiteral; import com.cloudbees.sdk.extensibility.ExtensionFinder; import com.cloudbees.sdk.extensibility.ExtensionPointList; import com.cloudbees.sdk.utils.Helper; import com.google.inject.*; import com.staxnet.appserver.utils.XmlHelper; import org.apache.commons.io.IOUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import javax.inject.Inject; import javax.inject.Singleton; import javax.xml.xpath.XPathExpressionException; import java.io.*; import java.net.URL; import java.net.URLClassLoader; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; /** * */ @Singleton public class CommandServiceImpl implements CommandService { private static final Logger LOGGER = Logger.getLogger(CommandServiceImpl.class.getName()); static final String NL = System.getProperty("line.separator"); @Inject private Injector injector; @Inject @ExtensionClassLoader private ClassLoader extClassLoader; @Inject private ExtensionPointList<CommandResolver> resolvers; @Inject private Verbose verbose; DirectoryStructure structure; List<Plugin> plugins = new ArrayList<Plugin>(); boolean localRepoLoaded; @Inject public CommandServiceImpl(DirectoryStructure structure) { this.structure = structure; } public void loadCommandProperties() { plugins = loadCommandFiles(structure.sdkRepository, structure.pluginExtension); localRepoLoaded = false; } private ArrayList<Plugin> loadCommandFiles(File dir, String fileExtension) { ArrayList<Plugin> plugins = new ArrayList<Plugin>(); String[] files = Helper.getFiles(dir, fileExtension); if (files != null) { for (String file : files) { plugins.addAll(loadPlugins(new File(dir, file))); } } localRepoLoaded = true; return plugins; } private ArrayList<Plugin> loadPlugins(File commandFile) { ArrayList<Plugin> plugins = new ArrayList<Plugin>(); try { Plugin plugin = parsePluginFile(commandFile); plugins.add(plugin); } catch (Exception e) { System.err.println("ERROR: Cannot parse file: " + commandFile); } return plugins; } private Plugin parsePluginFile(File file) throws FileNotFoundException, XPathExpressionException { InputStream inputStream = new FileInputStream(file); try { InputSource input = new InputSource(inputStream); Document doc = XmlHelper.readXML(input); Plugin plugin = new Plugin(); Element e = doc.getDocumentElement(); if (e.getTagName().equalsIgnoreCase("plugin")) { if (e.hasAttribute("artifact")) plugin.setArtifact(e.getAttribute("artifact")); NodeList nodes = e.getChildNodes(); List<String> jars = new ArrayList<String>(); plugin.setJars(jars); List<CommandProperties> commands = new ArrayList<CommandProperties>(); plugin.setProperties(commands); for (int i = 0; i < nodes.getLength(); i++) { Node node = nodes.item(i); if (node.getNodeName().equals("jar")) jars.add(node.getTextContent().trim()); else if (node.getNodeName().equals("command")) { CommandProperties commandProperties = new CommandProperties(); commandProperties.setGroup(getAttribute(node, "group")); commandProperties.setName(getAttribute(node, "name")); commandProperties.setPattern(getAttribute(node, "pattern")); commandProperties.setDescription(getAttribute(node, "description")); commandProperties.setClassName(getAttribute(node, "className")); String str = getAttribute(node, "experimental"); if (str != null) commandProperties.setExperimental(Boolean.parseBoolean(str)); str = getAttribute(node, "priority"); if (str != null) commandProperties.setPriority(Integer.parseInt(str)); commands.add(commandProperties); } } } return plugin; } finally { IOUtils.closeQuietly(inputStream); } } private String getAttribute(Node node, String attr) { return getAttribute(node, attr, null); } private String getAttribute(Node node, String attr, String defaultValue) { Node attrNode = node.getAttributes().getNamedItem(attr); if (attrNode == null) return defaultValue; else return attrNode.getNodeValue(); } public ACommand getCommand(String name) throws IOException { PluginCommand pluginCommand = getPluginCommand(name, plugins); // Look for additional command definition in the local repository if (pluginCommand == null) { if (!localRepoLoaded) { List<Plugin> localRepoCmds = loadCommandFiles(structure.getPluginDir(), structure.pluginExtension); plugins.addAll(localRepoCmds); pluginCommand = getPluginCommand(name, localRepoCmds); } } ACommand command = null; if (pluginCommand != null) { command = getCommand(name, pluginCommand); } else { // Try to find the plugin via bindings for (Plugin plugin : plugins) { command = getInjectorCommand(name, plugin.getJars()); if (command != null) { pluginCommand = new PluginCommand(plugin, null); break; } } } return command; } public int getCount() { return plugins.size(); } public String getHelp(URL helpTitleFile, String groupHelp, boolean all) { StringBuilder sb = new StringBuilder(getHelpTitle(helpTitleFile)); Map<String, List<CommandProperties>> map = new LinkedHashMap<String, List<CommandProperties>>(); if (!localRepoLoaded) plugins.addAll(loadCommandFiles(structure.getPluginDir(), structure.pluginExtension)); for (Plugin plugin : plugins) { setPluginCommandProperties(plugin, map, all); } buildHelp(sb, groupHelp, map); return sb.toString(); } private void setPluginCommandProperties(Plugin plugin, Map<String, List<CommandProperties>> map, boolean all) { for (CommandProperties cmd : plugin.getProperties()) { if (cmd.getGroup() != null && (!cmd.isExperimental() || all)) { List<CommandProperties> list = map.get(cmd.getGroup()); if (list == null) { list = new ArrayList<CommandProperties>(); map.put(cmd.getGroup(), list); } list.add(cmd); Collections.sort(list); } } } private void buildHelp(StringBuilder sb, String groupHelp, Map<String, List<CommandProperties>> map) { for (String group : map.keySet()) { sb.append(NL).append(group).append(" ").append(groupHelp).append(NL); for (CommandProperties cmd : map.get(group)) { sb.append(" ").append(Helper.getPaddedString(cmd.getName(), 30)); if (cmd.getDescription() != null) sb.append(cmd.getDescription()).append(NL); else sb.append(NL); } } } public String getHelp(Plugin plugin, String groupHelp, boolean all) { StringBuilder sb = new StringBuilder(); Map<String, List<CommandProperties>> map = new LinkedHashMap<String, List<CommandProperties>>(); setPluginCommandProperties(plugin, map, all); buildHelp(sb, groupHelp, map); return sb.toString(); } public List<GAV> getPlugins() { List<GAV> gavs = new ArrayList<GAV>(); if (!localRepoLoaded) plugins.addAll(loadCommandFiles(structure.getPluginDir(), structure.pluginExtension)); for (Plugin plugin : plugins) { if (plugin.getArtifact() != null) gavs.add(new GAV(plugin.getArtifact())); } return gavs; } public Plugin getPlugin(String name) { if (!localRepoLoaded) plugins.addAll(loadCommandFiles(structure.getPluginDir(), structure.pluginExtension)); for (Plugin plugin : plugins) { if (plugin.getArtifact() != null) { GAV gav = new GAV(plugin.getArtifact()); if (gav.artifactId.equalsIgnoreCase(name)) { return plugin; } } } return null; } public GAV deletePlugin(String name) { Plugin plugin = getPlugin(name); if (plugin != null) { GAV gav = new GAV(plugin.getArtifact()); File file = new File(structure.getPluginDir(), gav.artifactId + structure.pluginExtension); if (file.delete()) return gav; } return null; } private StringBuffer getHelpTitle(URL helpTitleFile) { StringBuffer sb = new StringBuffer(); BufferedReader reader = null; try { reader = new BufferedReader(new InputStreamReader(helpTitleFile.openStream())); String line; while ((line = reader.readLine()) != null) { sb.append(line).append(NL); } } catch (IOException ex) { System.err.println("ERROR: Cannot find help file: " + helpTitleFile); } finally { if (reader != null) try { reader.close(); } catch (IOException ignored) { } } return sb; } private PluginCommand getPluginCommand(String commandName, List<Plugin> plugins) { PluginCommand pluginCommand = null; for (Plugin plugin : plugins) { for (CommandProperties commandProperties : plugin.getProperties()) { if (commandName.matches(commandProperties.getPattern())) { if (pluginCommand != null) { if (commandProperties.getPriority() < pluginCommand.commandProperties.getPriority()) pluginCommand = new PluginCommand(plugin, commandProperties); } else pluginCommand = new PluginCommand(plugin, commandProperties); } } } return pluginCommand; } /** * Look up a command from Guice, */ private ACommand getInjectorCommand(String name, List<String> jars) throws IOException { try { Injector injector = this.injector; if (jars != null) { List<URL> urls = new ArrayList<URL>(); for (String jar : jars) { urls.add(new File(jar).toURI().toURL()); } ClassLoader pluginClassLoader = createClassLoader(urls, extClassLoader); injector = createChildModule(injector, pluginClassLoader); } // CommandResolvers take precedence over our default for (CommandResolver cr : resolvers.list(injector)) { ACommand cmd = cr.resolve(name); if (cmd!=null) return cmd; } Provider<ACommand> p; try { p = injector.getProvider(Key.get(ACommand.class, AnnotationLiteral.of(CLICommand.class, name))); } catch (ConfigurationException e) { if (verbose.isVerbose()) LOGGER.log(Level.WARNING, "failed to find the command", e); return null; // failed to find the command } return p.get(); } catch (Throwable e) { throw (IOException) new IOException("Failed to resolve command: " + name).initCause(e); } } /** * Creates a classloader from all the artifacts resolved thus far. */ private ClassLoader createClassLoader(List<URL> urls, ClassLoader parent) { // if (urls.isEmpty()) return parent; // nothing to load // this makes it hard to differentiate newly loaded stuff from what's already visible return new URLClassLoader(urls.toArray(new URL[urls.size()]), parent); } private ACommand getCommand(String name, PluginCommand pluginCommand) throws IOException { ACommand command; try { Injector injector = this.injector; ClassLoader pluginClassLoader = this.extClassLoader; String className = pluginCommand.commandProperties.getClassName(); List<String> jars = pluginCommand.plugin.getJars(); if (jars != null) { List<URL> urls = new ArrayList<URL>(); for (String jar : jars) { urls.add(new File(jar).toURI().toURL()); } pluginClassLoader = createClassLoader(urls, pluginClassLoader); injector = createChildModule(injector, pluginClassLoader); } Provider<ACommand> p; try { Class cl = Class.forName(className, true, pluginClassLoader); p = injector.getProvider(Key.get(cl)); } catch (ConfigurationException e) { throw (IOException)new IOException("Failed to instantiate a command: "+name).initCause(e); } command = p.get(); } catch (Exception e) { throw (IOException) new IOException("Failed to resolve command: " + name).initCause(e); } return command; } protected Injector createChildModule(Injector parent, final ClassLoader cl) throws InstantiationException, IOException { final List<Module> childModules = new ArrayList<Module>(); childModules.add(new ExtensionFinder(cl) { @Override protected <T> void bind(Class<? extends T> impl, Class<T> extensionPoint) { if (impl.getClassLoader() != cl) return; // only add newly discovered stuff // install CLIModules if (extensionPoint == CLIModule.class) { try { install((Module) impl.newInstance()); } catch (InstantiationException e) { throw (Error) new InstantiationError().initCause(e); } catch (IllegalAccessException e) { throw (Error) new IllegalAccessError().initCause(e); } return; } super.bind(impl, extensionPoint); } }); return parent.createChildInjector(childModules); } class PluginCommand { Plugin plugin; CommandProperties commandProperties; PluginCommand() { } PluginCommand(Plugin plugin, CommandProperties commandProperties) { this.plugin = plugin; this.commandProperties = commandProperties; } public Plugin getPlugin() { return plugin; } public void setPlugin(Plugin plugin) { this.plugin = plugin; } public CommandProperties getCommandProperties() { return commandProperties; } public void setCommandProperties(CommandProperties commandProperties) { this.commandProperties = commandProperties; } } }