/*
* JBoss, Home of Professional Open Source.
* Copyright 2011, Red Hat Middleware LLC, and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.as.model.test;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarFile;
import java.util.regex.Pattern;
import org.eclipse.aether.collection.DependencyCollectionException;
import org.eclipse.aether.resolution.DependencyResolutionException;
import org.jboss.logging.Logger;
import org.jboss.modules.filter.ClassFilter;
import org.xnio.IoUtils;
/**
*
* @author <a href="kabir.khan@jboss.com">Kabir Khan</a>
*/
public class ChildFirstClassLoaderBuilder {
/** Use this property on the lightning runs to make sure that people have set the root and cache properties */
private static final String STRICT_PROPERTY = "org.jboss.model.test.cache.strict";
/** Either the name of a parent directory e.g. "jboss-as", or a list of directories/files known to exist within that directory e.g. "[pom.xml, testsuite]"*/
private static final String ROOT_PROPERTY = "org.jboss.model.test.cache.root";
/** The relative location of the cache directory to the directory indicated by {@link #ROOT_PROPERTY} */
private static final String CACHE_FOLDER_PROPERTY = "org.jboss.model.test.classpath.cache";
/** A comma separated list of maven repository urls. If not set it will use http://repository.jboss.org/nexus/content/groups/developer/ */
static final String MAVEN_REPOSITORY_URLS = "org.jboss.model.test.maven.repository.urls";
private final MavenUtil mavenUtil;
private final File cache;
private final Set<URL> classloaderURLs = new LinkedHashSet<URL>();
private final Set<Pattern> parentFirst = new LinkedHashSet<Pattern>();
private final Set<Pattern> childFirst = new LinkedHashSet<Pattern>();
private ClassFilter parentExclusionFilter;
Map<URL, Set<String>> singleClassesByUrl = new HashMap<>();
private static final Logger log = Logger.getLogger(ChildFirstClassLoaderBuilder.class);
public ChildFirstClassLoaderBuilder(boolean useEapRepository) {
this.mavenUtil = MavenUtil.create(useEapRepository);
final String root = System.getProperty(ROOT_PROPERTY);
final String cacheFolderName = System.getProperty(CACHE_FOLDER_PROPERTY);
if (root == null && cacheFolderName == null) {
if (System.getProperty(STRICT_PROPERTY) != null) {
throw new IllegalStateException("Please use the " + ROOT_PROPERTY + " and " + CACHE_FOLDER_PROPERTY + " system properties to take advantage of cached classpaths");
}
cache = new File("target", "cached-classloader");
cache.mkdirs();
if (!cache.exists()) {
throw new IllegalStateException("Could not create cache file");
}
log.info("To optimize this test use the " + ROOT_PROPERTY + " and " + CACHE_FOLDER_PROPERTY + " system properties to take advantage of cached classpaths");
} else if (root != null && cacheFolderName != null){
if (cacheFolderName.indexOf('/') != -1 && cacheFolderName.indexOf('\\') != -1){
throw new IllegalStateException("Please use either '/' or '\\' as a file separator");
}
File file = new File(".").getAbsoluteFile();
final String[] rootChildren = root.startsWith("[") && root.endsWith("]") ? root.substring(1, root.length() - 1).split(",") : null;
if (rootChildren.length > 1) {
for (int i = 0 ; i < rootChildren.length ; i++) {
if (rootChildren[i].indexOf("/") != -1 || rootChildren[i].indexOf("\\") != -1) {
throw new IllegalStateException("Children must be direct children");
}
rootChildren[i] = rootChildren[i].trim();
}
}
while (file != null) {
if (rootChildren == null) {
if (file.getName().equals(root)) {
break;
}
} else {
boolean hasAllChildren = true;
for (String child : rootChildren) {
if (!new File(file, child).exists()) {
hasAllChildren = false;
break;
}
}
if (hasAllChildren) {
break;
}
}
file = file.getParentFile();
}
if (file != null) {
String separator = cacheFolderName.contains("/") ? "/" : "\\\\";
for (String part : cacheFolderName.split(separator)) {
file = new File(file, part);
if (file.exists()) {
if (!file.isDirectory()) {
throw new IllegalStateException(file.getAbsolutePath() + " is not a directory");
}
} else {
if (!file.mkdir()) {
if (!file.exists()) {
throw new IllegalStateException(file.getAbsolutePath() + " could not be created");
}
}
}
}
cache = file;
} else if (System.getProperty(STRICT_PROPERTY) != null) {
throw new IllegalStateException("Could not find a parent file called '" + root + "'");
} else {
// Probably running in an IDE where the working dir is not the source code root
cache = new File("target", "cached-classloader");
cache.mkdirs();
if (!cache.exists()) {
throw new IllegalStateException("Could not create cache file");
}
}
} else {
throw new IllegalStateException("You must either set both " + ROOT_PROPERTY + " and " + CACHE_FOLDER_PROPERTY + ", or none of them");
}
}
public ChildFirstClassLoaderBuilder addURL(URL url) {
classloaderURLs.add(url);
return this;
}
public ChildFirstClassLoaderBuilder addSimpleResourceURL(String resource) throws MalformedURLException {
URL url = ChildFirstClassLoader.class.getResource(resource);
if (url == null) {
ClassLoader cl = ChildFirstClassLoader.class.getClassLoader();
if (cl == null) {
cl = ClassLoader.getSystemClassLoader();
}
url = cl.getResource(resource);
if (url == null) {
File file = new File(resource);
if (file.exists()) {
url = file.toURI().toURL();
}
}
}
if (url == null) {
throw new IllegalArgumentException("Could not find resource " + resource);
}
classloaderURLs.add(url);
return this;
}
public ChildFirstClassLoaderBuilder addMavenResourceURL(String artifactGav) throws IOException, ClassNotFoundException {
final String name = "maven-" + escape(artifactGav);
final File file = new File(cache, name);
if (file.exists()) {
log.trace("Using cached maven url for " + artifactGav + " from " + file.getAbsolutePath());
final ObjectInputStream in = new ObjectInputStream(new BufferedInputStream(new FileInputStream(file)));
try {
classloaderURLs.add((URL)in.readObject());
} catch (Exception e) {
log.warn("Error loading cached maven url for " + artifactGav + " from " + file.getAbsolutePath());
throw e;
} finally {
IoUtils.safeClose(in);
}
} else {
log.trace("No cached maven url for " + artifactGav + " found. " + file.getAbsolutePath() + " does not exist.");
final URL url = mavenUtil.createMavenGavURL(artifactGav);
classloaderURLs.add(url);
final ObjectOutputStream out = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(file)));
try {
out.writeObject(url);
} catch (Exception e) {
log.warn("Error writing cached maven url for " + artifactGav + " to " + file.getAbsolutePath());
throw e;
} finally {
IoUtils.safeClose(out);
}
}
return this;
}
public ChildFirstClassLoaderBuilder addRecursiveMavenResourceURL(String artifactGav, String... excludes)
throws DependencyCollectionException, DependencyResolutionException, IOException, ClassNotFoundException {
final String name = "maven-recursive-" + escape(artifactGav);
final File file = new File(cache, name);
if (file.exists()) {
log.trace("Using cached recursive maven urls for " + artifactGav + " from " + file.getAbsolutePath());
final ObjectInputStream in = new ObjectInputStream(new BufferedInputStream(new FileInputStream(file)));
try {
classloaderURLs.addAll((List<URL>)in.readObject());
} catch (Exception e) {
log.warn("Error loading cached recursive maven urls for " + artifactGav + " from " + file.getAbsolutePath());
throw e;
} finally {
IoUtils.safeClose(in);
}
} else {
log.trace("No cached recursive maven urls for " + artifactGav + " found. " + file.getAbsolutePath() + " does not exist.");
final List<URL> urls = mavenUtil.createMavenGavRecursiveURLs(artifactGav, excludes);
classloaderURLs.addAll(urls);
final ObjectOutputStream out = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(file)));
try {
out.writeObject(urls);
} catch (Exception e) {
log.warn("Error writing cached recursive maven urls for " + artifactGav + " to " + file.getAbsolutePath());
throw e;
} finally {
IoUtils.safeClose(out);
}
}
return this;
}
public ChildFirstClassLoaderBuilder addParentFirstClassPattern(String pattern) {
parentFirst.add(compilePattern(pattern));
return this;
}
public ChildFirstClassLoaderBuilder addChildFirstClassPattern(String pattern) {
childFirst.add(compilePattern(pattern));
return this;
}
public ChildFirstClassLoaderBuilder excludeFromParent(ClassFilter filter) {
parentExclusionFilter = filter;
return this;
}
private String escape(String artifactGav) {
return artifactGav.replaceAll(":", "-x-");
}
public URLClassLoader build() {
//Put the singleClassesByUrl classes into classloaderURLs
for (Map.Entry<URL, Set<String>> entry : singleClassesByUrl.entrySet()) {
if (classloaderURLs.contains(entry.getKey())) {
throw new IllegalStateException("Url " + entry.getKey() + " which is the code source for the following classes has "
+ "already been set up via other means: " + entry.getValue());
}
classloaderURLs.add(entry.getKey());
//Now add the classes for the URL as child first classes
Set<String> childFirstNames = new HashSet<String>();
for (String clazz : entry.getValue()) {
childFirst.add(compilePattern(clazz));
childFirstNames.add(clazz);
}
//Then get all the other classes for the URL and add as parent first classes
try {
File file = new File(entry.getKey().toURI());
if (file.isDirectory()) {
addParentFirstPatternsFromDirectory(file, "");
} else if (file.getName().endsWith(".jar")){
addParentFirstPatternsFromJar(file);
} else {
//TODO - implement something like addParentFirstPatternsFromJar if that becomes needed
throw new IllegalStateException("Single class exclusions from jar files is not working: " + entry);
}
} catch (URISyntaxException e) {
throw new IllegalStateException(e);
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
ClassLoader parent = this.getClass().getClassLoader() != null ? this.getClass().getClassLoader() : null;
return new ChildFirstClassLoader(parent, parentFirst, childFirst, parentExclusionFilter, classloaderURLs.toArray(new URL[classloaderURLs.size()]));
}
private void addParentFirstPatternsFromDirectory(File directory, String prefix) {
for (File file : directory.listFiles()) {
if (file.isDirectory()) {
addParentFirstPatternsFromDirectory(file, prefix + file.getName() + ".");
} else {
final String fileName = file.getName();
if (fileName.endsWith(".class")) {
parentFirst.add(compilePattern(trimDotClass(prefix + fileName)));
}
}
}
}
private void addParentFirstPatternsFromJar(File jar) throws IOException {
JarFile jarFile = new JarFile(jar);
jarFile.stream()
.filter(jarEntry -> !jarEntry.isDirectory() && jarEntry.getName().endsWith(".class"))
.map(jarEntry -> compileJarEntryPattern(trimDotClass(jarEntry.getName())))
.forEach(pattern -> parentFirst.add(pattern));
}
private String trimDotClass(String fileName) {
return fileName.substring(0, fileName.length() - ".class".length());
}
private Pattern compileJarEntryPattern(String pattern) {
return compilePattern(pattern.replace("/", "."));
}
private Pattern compilePattern(String pattern) {
return Pattern.compile(pattern.replace(".", "\\.").replace("*", ".*"));
}
public ChildFirstClassLoaderBuilder addSingleChildFirstClass(Class<?>...classes) {
for (Class<?> clazz : classes) {
URL url = clazz.getProtectionDomain().getCodeSource().getLocation();
Set<String> classSet = singleClassesByUrl.get(url);
if (classSet == null) {
classSet = new HashSet<String>();
singleClassesByUrl.put(url, classSet);
}
classSet.add(clazz.getName());
}
return this;
}
}