/* * MIT License * * Copyright (c) 2020-present Cloudogu GmbH and Contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ 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.lifecycle.classloading.ClassLoaderLifeCycle; 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.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; import static java.util.stream.Collectors.toSet; //~--- JDK imports ------------------------------------------------------------ /** * * @author Sebastian Sdorra * * TODO don't mix nio and io */ @SuppressWarnings("squid:S3725") // performance is not critical, for this type of checks 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 final SmpDescriptorExtractor extractor = new SmpDescriptorExtractor(); 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(InstalledPluginDescriptor.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 installedPlugins = findInstalledPlugins(); logger.debug("found {} installed plugins", installedPlugins.size()); Set newlyInstalledPlugins = installPending(installedPlugins); logger.debug("finished installation of {} plugins", newlyInstalledPlugins.size()); Set plugins = concat(installedPlugins, newlyInstalledPlugins); logger.trace("start building plugin tree"); PluginTree pluginTree = new PluginTree(plugins); logger.info("install plugin tree:\n{}", pluginTree); List leafLastNodes = pluginTree.getLeafLastNodes(); logger.trace("create plugin wrappers and build classloaders"); Set wrappers = createPluginWrappers(classLoader, leafLastNodes); logger.debug("collected {} plugins", wrappers.size()); return ImmutableSet.copyOf(wrappers); } private Set concat(Set installedPlugins, Set newlyInstalledPlugins) { // We first add all newly installed plugins, // after that we add the missing plugins, which are already installed. // ExplodedSmp is equal by its path, so duplicates (updates) are not in the result. return ImmutableSet.builder() .addAll(newlyInstalledPlugins) .addAll(installedPlugins) .build(); } private Set installPending(Set installedPlugins) throws IOException { Set archives = collect(pluginDirectory, new PluginArchiveFilter()); logger.debug("start installation of {} pending archives", archives.size()); Map pending = new HashMap<>(); for (Path archive : archives) { pending.put(archive, extractor.extractPluginDescriptor(archive)); } PluginInstallationContext installationContext = PluginInstallationContext.fromDescriptors( installedPlugins.stream().map(ExplodedSmp::getPlugin).collect(toSet()), pending.values() ); for (Map.Entry entry : pending.entrySet()) { try { PluginInstallationVerifier.verify(installationContext, entry.getValue()); } catch (PluginException ex) { Path path = entry.getKey(); logger.error("failed to install smp {}, because it could not be verified", path); logger.error("to restore scm-manager functionality remove the smp file {} from the plugin directory", path); throw ex; } } return extract(archives); } private Set findInstalledPlugins() throws IOException { return collectPluginDirectories(pluginDirectory) .stream() .filter(isPluginDirectory()) .map(ExplodedSmp::create) .collect(Collectors.toSet()); } private Predicate isPluginDirectory() { return dir -> Files.exists(dir.resolve(DIRECTORY_METAINF).resolve("scm").resolve("plugin.xml")); } /** * 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()) { InstalledPlugin 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+ } } InstalledPlugin plugin = createPlugin(createParentPluginClassLoader(classLoader, parents), smp); if (plugin != null) { node.setWrapper(plugin); plugins.add(plugin); } } /** * 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) { paths.add(parent); } } return paths.build(); } /** * Method description * * * @param parentClassLoader * @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()]); InstalledPluginDescriptor plugin = smp.getPlugin(); String id = plugin.getInformation().getName(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; } private InstalledPluginDescriptor createDescriptor(ClassLoader classLoader, Path descriptor) { ClassLoader ctxcl = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(classLoader); try { return (InstalledPluginDescriptor) 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); } } private InstalledPlugin createPlugin(ClassLoader classLoader, ExplodedSmp smp) throws IOException { InstalledPlugin plugin = null; Path directory = smp.getPath(); Path descriptorPath = directory.resolve(PluginConstants.FILE_DESCRIPTOR); if (Files.exists(descriptorPath)) { boolean core = Files.exists(directory.resolve(PluginConstants.FILE_CORE)); ClassLoader cl = createClassLoader(classLoader, smp); InstalledPluginDescriptor descriptor = createDescriptor(cl, descriptorPath); WebResourceLoader resourceLoader = createWebResourceLoader(directory); plugin = new InstalledPlugin(descriptor, cl, resourceLoader, directory, core); } else { logger.warn("found plugin directory without plugin descriptor"); } return plugin; } /** * Method description * * * * @param classLoader * @param nodes * * @return * * @throws IOException */ private Set createPluginWrappers(ClassLoader classLoader, List nodes) throws IOException { Set plugins = Sets.newHashSet(); for (PluginNode node : nodes) { appendPluginWrapper(plugins, classLoader, node); } 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 Set extract(Iterable archives) throws IOException { logger.debug("extract archives"); ImmutableSet.Builder extracted = ImmutableSet.builder(); 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); extracted.add(ExplodedSmp.create(directory.toPath())); } return extracted.build(); } /** * 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; }