Files
SCM-Manager/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java
Florian Scholdei f1b34eb502 Changeover to MIT license (#1066)
* prepare license-maven-plugin for license migration
* added license mapping for tsx files and added some more excludes
* Changeover to MIT license
* Fix build problems
* Delete old remaining licenses
* Add more exclude path for license checker
* Rename included netbeans license, add exclude .m2/repository/
* Specify .m2 exclude because not only repository/, also wrapper/ must match
* Add .cache/ exclude for license check
* Modify formatting of license in java classes to comply with convention and IDE
* Add IntelliJ documentation for license configuration
* Update CHANGELOG.md
* Exclude tmp/workspace/ dir for license check
* Edit README.md

Co-authored-by: Sebastian Sdorra <sebastian.sdorra@cloudogu.com>
2020-03-23 15:35:58 +01:00

674 lines
16 KiB
Java

/*
* 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 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;
import java.util.function.Predicate;
import static java.util.stream.Collectors.toList;
//~--- 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 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<Path> stream(Path directory,
Filter<Path> filter)
throws IOException
{
return Files.newDirectoryStream(directory, filter);
}
/**
* Method description
*
*
* @param classLoader
* @return
*
* @throws IOException
*/
public Set<InstalledPlugin> collectPlugins(ClassLoader classLoader)
throws IOException
{
logger.info("collect plugins");
Set<Path> archives = collect(pluginDirectory, new PluginArchiveFilter());
logger.debug("extract {} archives", archives.size());
extract(archives);
List<Path> dirs =
collectPluginDirectories(pluginDirectory)
.stream()
.filter(isPluginDirectory())
.collect(toList());
logger.debug("process {} directories: {}", dirs.size(), dirs);
List<ExplodedSmp> smps = Lists.transform(dirs, new PathTransformer());
logger.trace("start building plugin tree");
PluginTree pluginTree = new PluginTree(smps);
logger.info("install plugin tree:\n{}", pluginTree);
List<PluginNode> leafLastNodes = pluginTree.getLeafLastNodes();
logger.trace("create plugin wrappers and build classloaders");
Set<InstalledPlugin> wrappers = createPluginWrappers(classLoader, leafLastNodes);
logger.debug("collected {} plugins", wrappers.size());
return ImmutableSet.copyOf(wrappers);
}
private Predicate<Path> 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<InstalledPlugin> plugins,
ClassLoader classLoader, PluginNode node)
throws IOException
{
if (node.getWrapper() != null) {
return;
}
ExplodedSmp smp = node.getPlugin();
List<ClassLoader> 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<Path> collect(Path directory, Filter<Path> filter)
throws IOException
{
Set<Path> paths;
try (DirectoryStream<Path> stream = stream(directory, filter))
{
paths = ImmutableSet.copyOf(stream);
}
return paths;
}
/**
* Method description
*
*
* @param directory
*
* @return
*
* @throws IOException
*/
private List<Path> collectPluginDirectories(Path directory) throws IOException
{
Builder<Path> paths = ImmutableList.builder();
Filter<Path> filter = new DirectoryFilter();
try (DirectoryStream<Path> 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<URL> 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<Path> 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<ClassLoader> 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<InstalledPlugin> createPluginWrappers(ClassLoader classLoader,
List<PluginNode> nodes)
throws IOException
{
Set<InstalledPlugin> 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 void extract(Iterable<Path> 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<Path>
{
/**
* 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<Path>
{
/**
* 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;
}