/*
* Copyright 2015 the original author or authors.
* 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 org.springframework.xd.module.options;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import org.springframework.boot.loader.archive.Archive;
import org.springframework.boot.loader.archive.ExplodedArchive;
import org.springframework.boot.loader.archive.JarFileArchive;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertyResolver;
import org.springframework.core.env.PropertySourcesPropertyResolver;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.xd.module.SimpleModuleDefinition;
import org.springframework.xd.module.options.ModuleOptions;
import org.springframework.xd.module.support.ArchiveResourceLoader;
import org.springframework.xd.module.support.NullClassLoader;
import org.springframework.xd.module.support.ParentLastURLClassLoader;
/**
* Contains utility methods for accessing a module's properties and dealing with ClassLoaders.
*
* @author Eric Bottard
* @author David Turanski
*/
public class ModuleUtils {
private static final List<String> DEFAULT_EXTRA_LIBS = Arrays.asList("/lib/*.jar", "/lib/*.zip");
private static final String MODULE_CLASSPATH_KEY = "module.classpath";
/**
* Used to resolve the module 'location'. Always a file: location at the time of writing.
*/
private static final PathMatchingResourcePatternResolver simpleResourceResolver = new PathMatchingResourcePatternResolver();
/**
* Create a ClassLoader suitable for running a module. Extra libraries can come from paths that are derived from
* module options. Any path that starts with a slash will be assumed to be an in-Archive entry, whereas any other
* path (including those starting with a protocol) will be dealt with by a classical resource pattern resolver.
*/
public static ClassLoader createModuleRuntimeClassLoader(SimpleModuleDefinition definition, ModuleOptions moduleOptions, ClassLoader parent) {
Resource moduleLocation = simpleResourceResolver.getResource(definition.getLocation());
Properties moduleProperties = loadModuleProperties(definition);
moduleProperties = moduleProperties == null ? new Properties() : moduleProperties;
String extraLibsCSV = moduleProperties.getProperty(MODULE_CLASSPATH_KEY, StringUtils.collectionToCommaDelimitedString(DEFAULT_EXTRA_LIBS));
List<String> extraLibs = new ArrayList<>();
String[] paths = extraLibsCSV.split("\\s*,\\s*");
MutablePropertySources propertySources = new MutablePropertySources();
propertySources.addFirst(moduleOptions.asPropertySource());
PropertyResolver placeHolderResolver = new PropertySourcesPropertyResolver(propertySources);
for (String path : paths) {
try {
extraLibs.add(placeHolderResolver.resolveRequiredPlaceholders(path));
}
catch (IllegalArgumentException ignored) {
}
}
return createModuleClassLoader(moduleLocation, parent, extraLibs);
}
/**
* Create a "simple" ClassLoader for a module, suitable for early discovery phase, before module options are known.
* Only the default library paths are used.
*/
public static ClassLoader createModuleDiscoveryClassLoader(Resource moduleLocation, ClassLoader parent) {
return createModuleClassLoader(moduleLocation, parent, DEFAULT_EXTRA_LIBS);
}
private static ClassLoader createModuleClassLoader(Resource moduleLocation, ClassLoader parent,
Iterable<String> patterns) {
try {
File moduleFile = moduleLocation.getFile();
Archive moduleArchive = moduleFile.isDirectory() ? new ExplodedArchive(moduleFile) : new JarFileArchive
(moduleFile);
List<URL> urls = new ArrayList<>();
ResourcePatternResolver resolver = new ArchiveResourceLoader(moduleArchive);
for (String pattern : patterns) {
for (Resource jar : resolver.getResources(pattern)) {
urls.add(jar.getURL());
}
}
// Add the module archive itself
urls.add(moduleArchive.getUrl());
return new ParentLastURLClassLoader(urls.toArray(new URL[urls.size()]), parent);
}
catch (IOException e) {
throw new RuntimeException("Exception creating module classloader for " + moduleLocation, e);
}
}
/**
* Return a resource that can be used to load the module '.properties' file (containing <i>e.g.</i> information
* about module options, or null if no such file exists.
*/
public static Resource modulePropertiesFile(SimpleModuleDefinition definition) {
return ModuleUtils.locateModuleResource(definition, ".properties");
}
/**
* Locate the module '.properties' file and load it as a {@link java.util.Properties} object.
* @return {@code null} if no properties file exists
*/
public static Properties loadModuleProperties(SimpleModuleDefinition moduleDefinition) {
Resource resource = modulePropertiesFile(moduleDefinition);
if (resource == null) {
return null;
}
Properties properties = new Properties();
try (InputStream inputStream = resource.getInputStream()) {
properties.load(inputStream);
return properties;
}
catch (IOException e) {
throw new RuntimeException(String.format("Unable to read module properties for %s:%s",
moduleDefinition.getName(), moduleDefinition.getType()), e);
}
}
/**
* Return an expected module resource given a file extension. Will throw an exception if more than one such
* resource exists. The resource is searched using an insulated module ClassLoader that only knows about the flat
* contents of the module archive (does not search any parent classloader, nor any additional module library).
*/
public static Resource locateModuleResource(SimpleModuleDefinition definition, String extension) {
Resource moduleLocation = simpleResourceResolver.getResource(definition.getLocation());
Assert.isTrue(moduleLocation.exists(), "module resource " + definition.getLocation() + " does not exist");
String ext = extension.startsWith(".") ? extension : "." + extension;
try {
URLClassLoader insulatedClassLoader = new ParentLastURLClassLoader(new URL[] {moduleLocation.getURL()}, NullClassLoader.NO_PARENT, true);
PathMatchingResourcePatternResolver moduleResolver = new PathMatchingResourcePatternResolver(insulatedClassLoader);
try {
Resource[] resources = moduleResolver.getResources("classpath:/config/*" + ext);
if (resources.length > 1) {
throw new IllegalStateException("Multiple top level module resources found :" + StringUtils
.arrayToCommaDelimitedString(resources));
}
else if (resources.length == 1) {
// Convert from ClassPathResource to UrlResource, as CPR.getInputStream()
// will apply caching, which we don't want we re-deploying. See XD-3141
Resource resource = resources[0];
if (resource instanceof ClassPathResource) {
return new UrlResource(resource.getURL());
} else {
return resource;
}
}
}
catch (IOException e) {
return null;
}
}
catch (IOException e) {
throw new RuntimeException("Exception creating module classloader for " + moduleLocation, e);
}
return null;
}
/**
* Return the resource that can be used to configure a module, or null if no such resource exists.
*
* @throws java.lang.IllegalStateException if both a .xml and .groovy file are present
*/
public static Resource resourceBasedConfigurationFile(SimpleModuleDefinition moduleDefinition) {
Resource xml = ModuleUtils.locateModuleResource(moduleDefinition, ".xml");
Resource groovy = ModuleUtils.locateModuleResource(moduleDefinition, ".groovy");
boolean xmlExists = xml != null;
boolean groovyExists = groovy != null;
if (xmlExists && groovyExists) {
throw new IllegalStateException(String.format("Found both resources '%s' and '%s' for module %s", xml,
groovy, moduleDefinition));
}
else if (xmlExists) {
return xml;
}
else if (groovyExists) {
return groovy;
}
else {
return null;
}
}
}