package org.codehaus.mojo.jaxb2.shared.environment.classloading;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
import org.apache.maven.plugin.logging.Log;
import org.codehaus.mojo.jaxb2.shared.Validate;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* <p>Utility class which assists in synthesizing a URLClassLoader for use as a ThreadLocal ClassLoader.
* Typical use:</p>
* <pre>
* <code>
* // Create and set the ThreadContext ClassLoader
* ThreadContextClassLoaderHolder holder = null;
*
* try {
*
* holder = ThreadContextClassLoaderBuilder.createFor(getClass())
* .addPath("some/path")
* .addURL(someURL)
* .addPaths(aPathList)
* .buildAndSet();
*
* // ... perform operations using the newly set ThreadContext ClassLoader...
*
* } finally {
* // Restore the original ClassLoader
* holder.restoreClassLoaderAndReleaseThread();
* }
* </code>
* </pre>
*
* @author <a href="mailto:lj@jguru.se">Lennart Jörelid</a>, jGuru Europe AB
* @since 2.0
*/
public final class ThreadContextClassLoaderBuilder {
// Internal state
private ClassLoader originalClassLoader;
private List<URL> urlList;
private Log log;
private String encoding;
private ThreadContextClassLoaderBuilder(final ClassLoader classLoader, final Log aLog, final String encoding) {
log = aLog;
originalClassLoader = classLoader;
urlList = new ArrayList<URL>();
this.encoding = encoding;
}
/**
* Adds the supplied anURL to the list of internal URLs which should be used to build an URLClassLoader.
* Will only add an URL once, and warns about trying to re-add an URL.
*
* @param anURL The URL to add.
* @return This ThreadContextClassLoaderBuilder, for builder pattern chaining.
*/
public ThreadContextClassLoaderBuilder addURL(final URL anURL) {
// Check sanity
Validate.notNull(anURL, "anURL");
// Add the segment unless already added.
for (URL current : urlList) {
if (current.toString().equalsIgnoreCase(anURL.toString())) {
if (log.isWarnEnabled()) {
log.warn("Not adding URL [" + anURL.toString() + "] twice. Check your plugin configuration.");
}
// Don't re-add the supplied URL.
return this;
}
}
// Add the supplied URL to the urlList
if (log.isDebugEnabled()) {
log.debug("Adding URL [" + anURL.toString() + "]");
}
//
// According to the URLClassLoader's documentation:
// "Any URL that ends with a '/' is assumed to refer to a directory.
// Otherwise, the URL is assumed to refer to a JAR file which will be downloaded and opened as needed."
//
// ... uhm ... instead of using the 'protocol' property of the URL itself?
//
// So ... we need to ensure that any file-protocol URLs which point to directories are actually
// terminated with a '/'. Otherwise the URLClassLoader treats those URLs as JARs - and hence ignores them.
//
urlList.add(addSlashToDirectoryUrlIfRequired(anURL));
return this;
}
/**
* Converts the supplied path to an URL and adds it to this ThreadContextClassLoaderBuilder.
*
* @param path A path to convert to an URL and add.
* @return This ThreadContextClassLoaderBuilder, for builder pattern chaining.
* @see #addURL(java.net.URL)
*/
public ThreadContextClassLoaderBuilder addPath(final String path) {
// Check sanity
Validate.notEmpty(path, "path");
// Convert to an URL, and delegate.
final URL anUrl;
try {
anUrl = new File(path).toURI().toURL();
} catch (MalformedURLException e) {
throw new IllegalArgumentException("Could not convert path [" + path + "] to an URL.", e);
}
// Delegate
return addURL(anUrl);
}
/**
* Converts the supplied path to an URL and adds it to this ThreadContextClassLoaderBuilder.
*
* @param paths A List of path to convert to URLs and add.
* @return This ThreadContextClassLoaderBuilder, for builder pattern chaining.
* @see #addPath(String)
*/
public ThreadContextClassLoaderBuilder addPaths(final List<String> paths) {
// Check sanity
Validate.notNull(paths, "paths");
// Delegate
for (String path : paths) {
addPath(path);
}
return this;
}
/**
* <p>This method performs 2 things in order:</p>
* <ol>
* <li>Builds a ThreadContext ClassLoader from the URLs supplied to this Builder, and assigns the
* newly built ClassLoader to the current Thread.</li>
* <li>Stores the ThreadContextClassLoaderHolder for later restoration.</li>
* </ol>
* References to the original ThreadContextClassLoader and the currentThread are stored within the returned
* ThreadContextClassLoaderHolder, and can be restored by a call to
* {@code ThreadContextClassLoaderHolder.restoreClassLoaderAndReleaseThread()}.
*
* @return A fully set up ThreadContextClassLoaderHolder which is used to set the
*/
public ThreadContextClassLoaderHolder buildAndSet() {
// Create the URLClassLoader from the supplied URLs
final URL[] allURLs = new URL[urlList.size()];
urlList.toArray(allURLs);
final URLClassLoader classLoader = new URLClassLoader(allURLs, originalClassLoader);
// Assign the ThreadContext ClassLoader
final Thread currentThread = Thread.currentThread();
currentThread.setContextClassLoader(classLoader);
// Build the classpath argument
StringBuilder builder = new StringBuilder();
try {
for (URL current : Collections.list(classLoader.getResources(""))) {
final String toAppend = getClassPathElement(current, encoding);
if (toAppend != null) {
builder.append(toAppend).append(File.pathSeparator);
}
}
} catch (Exception e) {
// Restore the original classloader to the active thread before failing.
currentThread.setContextClassLoader(originalClassLoader);
throw new IllegalStateException("Could not synthesize classpath from original classloader.", e);
}
final String classPathString = builder.length() > 0
? builder.toString().substring(0, builder.length() - File.pathSeparator.length())
: "";
// All done.
return new DefaultHolder(currentThread, this.originalClassLoader, classPathString);
}
/**
* Creates a new ThreadContextClassLoaderBuilder using the supplied original classLoader, as well
* as the supplied Maven Log.
*
* @param classLoader The original ClassLoader which should be used as the parent for the ThreadContext
* ClassLoader produced by the ThreadContextClassLoaderBuilder generated by this builder method.
* Cannot be null.
* @param log The active Maven Log. Cannot be null.
* @param encoding The encoding used by Maven. Cannot be null.
* @return A ThreadContextClassLoaderBuilder wrapping the supplied members.
*/
public static ThreadContextClassLoaderBuilder createFor(final ClassLoader classLoader,
final Log log,
final String encoding) {
// Check sanity
Validate.notNull(classLoader, "classLoader");
Validate.notNull(log, "log");
// All done.
return new ThreadContextClassLoaderBuilder(classLoader, log, encoding);
}
/**
* Creates a new ThreadContextClassLoaderBuilder using the original ClassLoader from the supplied Class, as well
* as the given Maven Log.
*
* @param aClass A non-null class from which to extract the original ClassLoader.
* @param log The active Maven Log. Cannot be null.
* @param encoding The encoding used by Maven. Cannot be null.
* @return A ThreadContextClassLoaderBuilder wrapping the supplied members.
*/
public static ThreadContextClassLoaderBuilder createFor(final Class<?> aClass,
final Log log,
final String encoding) {
// Check sanity
Validate.notNull(aClass, "aClass");
// Delegate
return createFor(aClass.getClassLoader(), log, encoding);
}
/**
* Converts the supplied URL to a class path element.
*
* @param anURL The non-null URL for which to acquire a classPath element.
* @param encoding The encoding used by Maven.
* @return The full (i.e. non-chopped) classpath element corresponding to the supplied URL.
* @throws java.lang.IllegalArgumentException if the supplied URL had an unknown protocol.
*/
public static String getClassPathElement(final URL anURL, final String encoding) throws IllegalArgumentException {
// Check sanity
Validate.notNull(anURL, "anURL");
final String protocol = anURL.getProtocol();
String toReturn = null;
if ("file".equalsIgnoreCase(protocol)) {
final String originalPath = anURL.getPath();
try {
return URLDecoder.decode(anURL.getPath(), encoding);
} catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException("Could not URLDecode path [" + originalPath
+ "] using encoding [" + encoding + "]", e);
}
} else if ("jar".equalsIgnoreCase(protocol)) {
toReturn = anURL.getPath();
} else if ("http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol)) {
toReturn = anURL.toString();
} else if ("bundleresource".equalsIgnoreCase(protocol)) { // e.g. when used in Eclipse/m2e
toReturn = anURL.toString();
} else {
throw new IllegalArgumentException("Unknown protocol [" + protocol + "]; could not handle URL ["
+ anURL + "]");
}
return toReturn;
}
//
// Private helpers
//
private URL addSlashToDirectoryUrlIfRequired(final URL anURL) {
// Check sanity
Validate.notNull(anURL, "anURL");
URL toReturn = anURL;
if ("file".equalsIgnoreCase(anURL.getProtocol())) {
final File theFile = new File(anURL.getPath());
if (theFile.isDirectory()) {
try {
// This ensures that an URL pointing to a File directory
// actually is terminated by a '/', which is required by
// the URLClassLoader to operate properly.
toReturn = theFile.toURI().toURL();
} catch (MalformedURLException e) {
// This should never happen
throw new IllegalArgumentException("Could not convert a File to an URL", e);
}
}
}
// All done.
return toReturn;
}
/**
* Default implementation of the ThreadContextClassLoaderCleaner specification,
* with added finalizer to ensure we release the Thread reference no matter
* what happens with any DefaultCleaner objects.
*/
@SuppressWarnings("all")
class DefaultHolder implements ThreadContextClassLoaderHolder {
// Internal state
private Thread affectedThread;
private ClassLoader originalClassLoader;
private String classPathArgument;
/**
* Compound constructor creating a default-implementation {@link ThreadContextClassLoaderHolder} which
* wraps references to the {@link Thread} affected as well as the original ClassLoader to restore during
* the call to {@link #restoreClassLoaderAndReleaseThread()} method.
*
* @param affectedThread The non-null Thread for which a new ClassLoader should be constructed.
* @param originalClassLoader The non-null original ClassLoader.
* @param classPathArgument The non-null classpath argument, to be returned
* from the method call to {@link #getClassPathAsArgument()}.
*/
public DefaultHolder(final Thread affectedThread,
final ClassLoader originalClassLoader,
final String classPathArgument) {
// Check sanity
Validate.notNull(affectedThread, "affectedThread");
Validate.notNull(originalClassLoader, "originalClassLoader");
Validate.notNull(classPathArgument, "classPathArgument");
// Assign internal state
this.affectedThread = affectedThread;
this.originalClassLoader = originalClassLoader;
this.classPathArgument = classPathArgument;
}
/**
* {@inheritDoc}
*/
@Override
public void restoreClassLoaderAndReleaseThread() {
if (affectedThread != null) {
// Restore original state
affectedThread.setContextClassLoader(originalClassLoader);
// Null out the internal state
affectedThread = null;
originalClassLoader = null;
classPathArgument = null;
}
}
/**
* {@inheritDoc}
*/
@Override
public String getClassPathAsArgument() {
return classPathArgument;
}
/**
* {@inheritDoc}
*/
@Override
protected void finalize() throws Throwable {
try {
// First, release all resources held by this object.
restoreClassLoaderAndReleaseThread();
} finally {
// Now, perform standard finalization.
super.finalize();
}
}
}
}