/** * Copyright (c) 2010, Sebastian Sdorra All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. 2. Redistributions in * binary form must reproduce the above copyright notice, this list of * conditions and the following disclaimer in the documentation and/or other * materials provided with the distribution. 3. Neither the name of SCM-Manager; * nor the names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * http://bitbucket.org/sdorra/scm-manager * */ package sonia.scm.plugin; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList.Builder; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.common.hash.Hashing; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.boot.ClassLoaderLifeCycle; import sonia.scm.plugin.ExplodedSmp.PathTransformer; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.net.URL; import java.nio.file.DirectoryStream; import java.nio.file.DirectoryStream.Filter; import java.nio.file.Files; import java.nio.file.Path; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Set; //~--- JDK imports ------------------------------------------------------------ /** * * @author Sebastian Sdorra * * TODO don't mix nio and io */ public final class PluginProcessor { /** Field description */ private static final String INSTALLEDNAME_FORMAT = "%s.%03d"; /** Field description */ private static final String DIRECTORY_CLASSES = "classes"; /** Field description */ private static final String DIRECTORY_DEPENDENCIES = "lib"; /** Field description */ private static final String DIRECTORY_INSTALLED = ".installed"; /** Field description */ private static final String DIRECTORY_METAINF = "META-INF"; /** Field description */ private static final String DIRECTORY_WEBAPP = "webapp"; /** Field description */ private static final String EXTENSION_PLUGIN = ".smp"; /** Field description */ private static final String FORMAT_DATE = "yyyy-MM-dd"; /** Field description */ private static final String GLOB_JAR = "*.jar"; /** * the logger for PluginProcessor */ private static final Logger logger = LoggerFactory.getLogger(PluginProcessor.class); //~--- constructors --------------------------------------------------------- private ClassLoaderLifeCycle classLoaderLifeCycle; /** * Constructs ... * * * @param classLoaderLifeCycle * @param pluginDirectory */ public PluginProcessor(ClassLoaderLifeCycle classLoaderLifeCycle, Path pluginDirectory) { this.classLoaderLifeCycle = classLoaderLifeCycle; this.pluginDirectory = pluginDirectory; this.installedDirectory = findInstalledDirectory(); try { this.context = JAXBContext.newInstance(Plugin.class); } catch (JAXBException ex) { throw new PluginLoadException("could not create jaxb context", ex); } } //~--- methods -------------------------------------------------------------- /** * Method description * * * @param directory * @param filter * * @return * * @throws IOException */ private static DirectoryStream stream(Path directory, Filter filter) throws IOException { return Files.newDirectoryStream(directory, filter); } /** * Method description * * * @param classLoader * @return * * @throws IOException */ public Set collectPlugins(ClassLoader classLoader) throws IOException { logger.info("collect plugins"); Set archives = collect(pluginDirectory, new PluginArchiveFilter()); logger.debug("extract {} archives", archives.size()); extract(archives); List dirs = collectPluginDirectories(pluginDirectory); logger.debug("process {} directories: {}", dirs.size(), dirs); List smps = Lists.transform(dirs, new PathTransformer()); logger.trace("start building plugin tree"); PluginTree pluginTree = new PluginTree(smps); logger.trace("build plugin tree: {}", pluginTree); List rootNodes = pluginTree.getRootNodes(); logger.trace("create plugin wrappers and build classloaders"); Set wrappers = createPluginWrappers(classLoader, rootNodes); logger.debug("collected {} plugins", wrappers.size()); return ImmutableSet.copyOf(wrappers); } /** * Method description * * * @param plugins * @param classLoader * @param node * * @throws IOException */ private void appendPluginWrapper(Set plugins, ClassLoader classLoader, PluginNode node) throws IOException { if (node.getWrapper() != null) { return; } ExplodedSmp smp = node.getPlugin(); List parents = Lists.newArrayList(); for (PluginNode parent : node.getParents()) { PluginWrapper wrapper = parent.getWrapper(); if (wrapper != null) { parents.add(wrapper.getClassLoader()); } else { //J- throw new PluginLoadException( String.format( "parent %s of plugin %s is not ready", parent.getId(), node.getId() ) ); //J+ } } PluginWrapper plugin = createPluginWrapper(createParentPluginClassLoader(classLoader, parents), smp); if (plugin != null) { node.setWrapper(plugin); plugins.add(plugin); } } /** * Method description * * * @param plugins * @param classLoader * @param nodes * * @throws IOException */ private void appendPluginWrappers(Set plugins, ClassLoader classLoader, List nodes) throws IOException { // TODO fix plugin loading order for (PluginNode node : nodes) { appendPluginWrapper(plugins, classLoader, node); } for (PluginNode node : nodes) { appendPluginWrappers(plugins, classLoader, node.getChildren()); } } /** * Method description * * * @param directory * @param filter * * @return * * @throws IOException */ private Set collect(Path directory, Filter filter) throws IOException { Set paths; try (DirectoryStream stream = stream(directory, filter)) { paths = ImmutableSet.copyOf(stream); } return paths; } /** * Method description * * * @param directory * * @return * * @throws IOException */ private List collectPluginDirectories(Path directory) throws IOException { Builder paths = ImmutableList.builder(); Filter filter = new DirectoryFilter(); try (DirectoryStream parentStream = stream(directory, filter)) { for (Path parent : parentStream) { try (DirectoryStream direcotries = stream(parent, filter)) { paths.addAll(direcotries); } } } return paths.build(); } /** * Method description * * * @param parentClassLoader * @param directory * @param smp * * @return * * @throws IOException */ private ClassLoader createClassLoader(ClassLoader parentClassLoader, ExplodedSmp smp) throws IOException { List urls = new ArrayList<>(); Path directory = smp.getPath(); Path metaDir = directory.resolve(DIRECTORY_METAINF); if (!Files.exists(metaDir)) { throw new FileNotFoundException("could not find META-INF directory"); } Path classesDir = directory.resolve(DIRECTORY_CLASSES); if (Files.exists(classesDir)) { urls.add(classesDir.toUri().toURL()); } Path libDir = directory.resolve(DIRECTORY_DEPENDENCIES); if (Files.exists(libDir)) { try (DirectoryStream pathDirectoryStream = Files.newDirectoryStream(libDir, GLOB_JAR)) { for (Path f : pathDirectoryStream) { urls.add(f.toUri().toURL()); } } } ClassLoader classLoader; URL[] urlArray = urls.toArray(new URL[urls.size()]); Plugin plugin = smp.getPlugin(); String id = plugin.getInformation().getId(false); if (smp.getPlugin().isChildFirstClassLoader()) { logger.debug("create child fist classloader for plugin {}", id); classLoader = classLoaderLifeCycle.createChildFirstPluginClassLoader(urlArray, parentClassLoader, id); } else { logger.debug("create parent fist classloader for plugin {}", id); classLoader = classLoaderLifeCycle.createPluginClassLoader(urlArray, parentClassLoader, id); } return classLoader; } /** * Method description * * * @return */ private String createDate() { return new SimpleDateFormat(FORMAT_DATE).format(new Date()); } /** * Method description * * * @param root * @param parents * * @return */ private ClassLoader createParentPluginClassLoader(ClassLoader root, List parents) { ClassLoader result; int size = parents.size(); if (size == 0) { result = root; } else if (size == 1) { result = parents.get(0); } else { result = new MultiParentClassLoader(parents); } return result; } /** * Method description * * * * @param classLoader * @param descriptor * * @return */ private Plugin createPlugin(ClassLoader classLoader, Path descriptor) { ClassLoader ctxcl = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(classLoader); try { return (Plugin) context.createUnmarshaller().unmarshal( descriptor.toFile()); } catch (JAXBException ex) { throw new PluginLoadException( "could not load plugin desriptor ".concat(descriptor.toString()), ex); } finally { Thread.currentThread().setContextClassLoader(ctxcl); } } /** * Method description * * * @param classLoader * @param directory * @param smp * * @return * * @throws IOException */ private PluginWrapper createPluginWrapper(ClassLoader classLoader, ExplodedSmp smp) throws IOException { PluginWrapper wrapper = null; Path directory = smp.getPath(); Path descriptor = directory.resolve(PluginConstants.FILE_DESCRIPTOR); if (Files.exists(descriptor)) { ClassLoader cl = createClassLoader(classLoader, smp); Plugin plugin = createPlugin(cl, descriptor); WebResourceLoader resourceLoader = createWebResourceLoader(directory); wrapper = new PluginWrapper(plugin, cl, resourceLoader, directory); } else { logger.warn("found plugin directory without plugin descriptor"); } return wrapper; } /** * Method description * * * * @param classLoader * @param smps * @param rootNodes * * @return * * @throws IOException */ private Set createPluginWrappers(ClassLoader classLoader, List rootNodes) throws IOException { Set plugins = Sets.newHashSet(); appendPluginWrappers(plugins, classLoader, rootNodes); return plugins; } /** * Method description * * * @param directory * * @return */ private WebResourceLoader createWebResourceLoader(Path directory) { WebResourceLoader resourceLoader; Path webapp = directory.resolve(DIRECTORY_WEBAPP); if (Files.exists(webapp)) { logger.debug("create WebResourceLoader for path {}", webapp); resourceLoader = new PathWebResourceLoader(webapp); } else { logger.debug("create empty WebResourceLoader"); resourceLoader = new EmptyWebResourceLoader(); } return resourceLoader; } /** * Method description * * * @param archives * * @throws IOException */ private void extract(Iterable archives) throws IOException { logger.debug("extract archives"); for (Path archive : archives) { File archiveFile = archive.toFile(); logger.trace("extract archive {}", archive); SmpArchive smp = SmpArchive.create(archive); logger.debug("extract plugin {}", smp.getPlugin()); File directory = PluginsInternal.createPluginDirectory(pluginDirectory.toFile(), smp.getPlugin()); String checksum = com.google.common.io.Files.hash(archiveFile, Hashing.sha256()).toString(); File checksumFile = PluginsInternal.getChecksumFile(directory); PluginsInternal.extract(smp, checksum, directory, checksumFile, false); moveArchive(archive); } } /** * Method description * * * @return */ private Path findInstalledDirectory() { Path directory = null; Path installed = pluginDirectory.resolve(DIRECTORY_INSTALLED); Path date = installed.resolve(createDate()); for (int i = 0; i < 999; i++) { Path dir = date.resolve(String.format("%03d", i)); if (!Files.exists(dir)) { directory = dir; break; } } if (directory == null) { throw new PluginException("could not find installed directory"); } return directory; } /** * Method description * * * @param archive * * @throws IOException */ private void moveArchive(Path archive) throws IOException { if (!Files.exists(installedDirectory)) { logger.debug("create installed directory {}", installedDirectory); Files.createDirectories(installedDirectory); } Path installed = null; for (int i = 0; i < 1000; i++) { String name = String.format(INSTALLEDNAME_FORMAT, archive.getFileName(), i); installed = installedDirectory.resolve(name); if (!Files.exists(installed)) { break; } } logger.debug("move installed archive to {}", installed); Files.move(archive, installed); } //~--- inner classes -------------------------------------------------------- /** * Class description * * * @version Enter version here..., 14/06/04 * @author Enter your name here... */ private static class DirectoryFilter implements DirectoryStream.Filter { /** * Method description * * * @param entry * * @return * * @throws IOException */ @Override public boolean accept(Path entry) throws IOException { return Files.isDirectory(entry) &&!entry.getFileName().toString().startsWith("."); } } /** * Class description * * * @version Enter version here..., 14/06/04 * @author Enter your name here... */ private static class PluginArchiveFilter implements DirectoryStream.Filter { /** * Method description * * * @param entry * * @return * * @throws IOException */ @Override public boolean accept(Path entry) throws IOException { return Files.isRegularFile(entry) && entry.getFileName().toString().endsWith(EXTENSION_PLUGIN); } } //~--- fields --------------------------------------------------------------- /** Field description */ private final JAXBContext context; /** Field description */ private final Path installedDirectory; /** Field description */ private final Path pluginDirectory; }