/*
* $Id$
* This file is a part of the Arakhne Foundation Classes, http://www.arakhne.org/afc
*
* Copyright (c) 2000-2012 Stephane GALLAND.
* Copyright (c) 2005-10, Multiagent Team, Laboratoire Systemes et Transports,
* Universite de Technologie de Belfort-Montbeliard.
* Copyright (c) 2013-2016 The original authors, and other 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.arakhne.afc.vmutil;
import java.io.File;
import java.io.FilePermission;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.SocketPermission;
import java.net.URL;
import java.net.URLConnection;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.CodeSigner;
import java.security.CodeSource;
import java.security.Permission;
import java.security.PermissionCollection;
import java.security.PrivilegedAction;
import java.security.PrivilegedExceptionAction;
import java.security.SecureClassLoader;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.Attributes.Name;
import java.util.jar.Manifest;
import java.util.regex.Pattern;
import org.eclipse.xtext.xbase.lib.Pure;
import org.arakhne.afc.vmutil.locale.Locale;
/** This class loader permits to load classes from
* a set of classpaths.
*
* @author $Author: sgalland$
* @version $FullVersion$
* @mavengroupid $GroupId$
* @mavenartifactid $ArtifactId$
*/
@SuppressWarnings({"restriction", "checkstyle:illegalimport"})
public class DynamicURLClassLoader extends SecureClassLoader {
/**
* The search path for classes and resources.
*/
protected sun.misc.URLClassPath ucp;
/** The context to be used when loading classes and resources.
*/
protected AccessControlContext acc;
/**
* Constructs a new ClassPathClassLoader for the given URLs. The URLs will be
* searched in the order specified for classes and resources after first
* searching in the specified parent class loader. 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.
*
* <p>If there is a security manager, this method first
* calls the security manager's <code>checkCreateClassLoader</code> method
* to ensure creation of a class loader is allowed.
*
* @param parent the parent class loader for delegation
* @param acc is the current access context
* @param urls the URLs from which to load classes and resources
* @throws SecurityException if a security manager exists and its
* <code>checkCreateClassLoader</code> method doesn't allow
* creation of a class loader.
* @see SecurityManager#checkCreateClassLoader
*/
protected DynamicURLClassLoader(ClassLoader parent, AccessControlContext acc, URL... urls) {
super(parent);
// this is to make the stack depth consistent with 1.1
final SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
this.ucp = new sun.misc.URLClassPath(mergeClassPath(urls));
this.acc = acc;
}
/**
* Appends the specified URL to the list of URLs to search for
* classes and resources.
*
* @param url the URL to be added to the search path of URLs
*/
public void addURL(final URL url) {
AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
DynamicURLClassLoader.this.ucp.addURL(url);
return null;
}
}, this.acc);
}
/**
* Appends the specified URL to the list of URLs to search for
* classes and resources.
*
* @param urls the URLs to be added to the search path of URLs
*/
public void addURLs(URL... urls) {
for (final URL url : urls) {
addURL(url);
}
}
/**
* Appends the specified URL to the list of URLs to search for
* classes and resources.
*
* @param urls the URL to be added to the search path of URLs
*/
public void removeURLs(URL... urls) {
final Set<URL> set = new HashSet<>();
set.addAll(Arrays.asList(this.ucp.getURLs()));
set.removeAll(Arrays.asList(urls));
final URL[] tab = new URL[set.size()];
set.toArray(tab);
this.ucp = new sun.misc.URLClassPath(tab);
}
/**
* Appends the specified URL to the list of URLs to search for
* classes and resources.
*
* @param url the URL to be added to the search path of URLs
*/
public void removeURL(URL url) {
removeURLs(url);
}
/**
* Returns the search path of URLs for loading classes and resources.
* This includes the original list of URLs specified to the constructor,
* along with any URLs subsequently appended by the addURL() method.
* @return the search path of URLs for loading classes and resources.
*/
@Pure
public URL[] getURLs() {
return this.ucp.getURLs();
}
/**
* Finds and loads the class with the specified name from the URL search
* path. Any URLs referring to JAR files are loaded and opened as needed
* until the class is found.
*
* @param name the name of the class
* @return the resulting class
* @exception ClassNotFoundException if the class could not be found
*/
@Override
@Pure
protected Class<?> findClass(final String name) throws ClassNotFoundException {
try {
return AccessController.doPrivileged(new PrivilegedExceptionAction<Class<?>>() {
@Override
public Class<?> run() throws ClassNotFoundException {
final String path = name.replace('.', '/').concat(".class"); //$NON-NLS-1$
final sun.misc.Resource res = DynamicURLClassLoader.this.ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
}
throw new ClassNotFoundException(name);
}
}, this.acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
}
/**
* Defines a Class using the class bytes obtained from the specified
* Resource. The resulting Class must be resolved before it can be
* used.
*
* @param name is the name of the class to define
* @param res is the resource from which the class byte-code could be obtained
* @return the loaded class.
* @throws IOException in case the byte-code was unavailable.
*/
protected Class<?> defineClass(String name, sun.misc.Resource res) throws IOException {
final int i = name.lastIndexOf('.');
final URL url = res.getCodeSourceURL();
if (i != -1) {
final String pkgname = name.substring(0, i);
// Check if package already loaded.
final Package pkg = getPackage(pkgname);
final Manifest man = res.getManifest();
if (pkg != null) {
// Package found, so check package sealing.
if (pkg.isSealed()) {
// Verify that code source URL is the same.
if (!pkg.isSealed(url)) {
throw new SecurityException(Locale.getString("E1", pkgname)); //$NON-NLS-1$
}
} else {
// Make sure we are not attempting to seal the package
// at this code source URL.
if ((man != null) && isSealed(pkgname, man)) {
throw new SecurityException(Locale.getString("E2", pkgname)); //$NON-NLS-1$
}
}
} else {
if (man != null) {
definePackage(pkgname, man, url);
} else {
definePackage(pkgname, null, null, null, null, null, null, null);
}
}
}
// Now read the class bytes and define the class
final java.nio.ByteBuffer bb = res.getByteBuffer();
if (bb != null) {
// Use (direct) ByteBuffer:
final CodeSigner[] signers = res.getCodeSigners();
final CodeSource cs = new CodeSource(url, signers);
return defineClass(name, bb, cs);
}
final byte[] b = res.getBytes();
// must read certificates AFTER reading bytes.
final CodeSigner[] signers = res.getCodeSigners();
final CodeSource cs = new CodeSource(url, signers);
return defineClass(name, b, 0, b.length, cs);
}
/**
* Defines a new package by name in this ClassLoader. The attributes
* contained in the specified Manifest will be used to obtain package
* version and sealing information. For sealed packages, the additional
* URL specifies the code source URL from which the package was loaded.
*
* @param name the package name
* @param man the Manifest containing package version and sealing
* information
* @param url the code source url for the package, or null if none
* @exception IllegalArgumentException if the package name duplicates
* an existing package either in this class loader or one
* of its ancestors
* @return the newly defined Package object
*/
@SuppressWarnings("checkstyle:npathcomplexity")
protected Package definePackage(String name, Manifest man, URL url) throws IllegalArgumentException {
final String path = name.replace('.', '/').concat("/"); //$NON-NLS-1$
String specTitle = null;
String specVersion = null;
String specVendor = null;
String implTitle = null;
String implVersion = null;
String implVendor = null;
String sealed = null;
URL sealBase = null;
Attributes attr = man.getAttributes(path);
if (attr != null) {
specTitle = attr.getValue(Name.SPECIFICATION_TITLE);
specVersion = attr.getValue(Name.SPECIFICATION_VERSION);
specVendor = attr.getValue(Name.SPECIFICATION_VENDOR);
implTitle = attr.getValue(Name.IMPLEMENTATION_TITLE);
implVersion = attr.getValue(Name.IMPLEMENTATION_VERSION);
implVendor = attr.getValue(Name.IMPLEMENTATION_VENDOR);
sealed = attr.getValue(Name.SEALED);
}
attr = man.getMainAttributes();
if (attr != null) {
if (specTitle == null) {
specTitle = attr.getValue(Name.SPECIFICATION_TITLE);
}
if (specVersion == null) {
specVersion = attr.getValue(Name.SPECIFICATION_VERSION);
}
if (specVendor == null) {
specVendor = attr.getValue(Name.SPECIFICATION_VENDOR);
}
if (implTitle == null) {
implTitle = attr.getValue(Name.IMPLEMENTATION_TITLE);
}
if (implVersion == null) {
implVersion = attr.getValue(Name.IMPLEMENTATION_VERSION);
}
if (implVendor == null) {
implVendor = attr.getValue(Name.IMPLEMENTATION_VENDOR);
}
if (sealed == null) {
sealed = attr.getValue(Name.SEALED);
}
}
if ("true".equalsIgnoreCase(sealed)) { //$NON-NLS-1$
sealBase = url;
}
return definePackage(name, specTitle, specVersion, specVendor,
implTitle, implVersion, implVendor, sealBase);
}
/*
* Returns true if the specified package name is sealed according to the
* given manifest.
*/
private static boolean isSealed(String name, Manifest man) {
final String path = name.replace('.', '/').concat("/"); //$NON-NLS-1$
Attributes attr = man.getAttributes(path);
String sealed = null;
if (attr != null) {
sealed = attr.getValue(Name.SEALED);
}
if (sealed == null) {
attr = man.getMainAttributes();
if (attr != null) {
sealed = attr.getValue(Name.SEALED);
}
}
return "true".equalsIgnoreCase(sealed); //$NON-NLS-1$
}
/**
* Finds the resource with the specified name on the URL search path.
*
* @param name the name of the resource
* @return a <code>URL</code> for the resource, or <code>null</code>
* if the resource could not be found.
*/
@Override
@Pure
public URL findResource(final String name) {
/*
* The same restriction to finding classes applies to resources
*/
final URL url = AccessController.doPrivileged(new PrivilegedAction<URL>() {
@Override
public URL run() {
return DynamicURLClassLoader.this.ucp.findResource(name, true);
}
}, this.acc);
return url != null ? this.ucp.checkURL(url) : null;
}
/**
* Returns an Enumeration of URLs representing all of the resources
* on the URL search path having the specified name.
*
* @param name the resource name
* @exception IOException if an I/O exception occurs
* @return an <code>Enumeration</code> of <code>URL</code>s
*/
@Override
@Pure
public Enumeration<URL> findResources(final String name) throws IOException {
final Enumeration<?> e = this.ucp.findResources(name, true);
return new Enumeration<URL>() {
private URL url;
private boolean next() {
if (this.url != null) {
return true;
}
do {
final URL u = AccessController.doPrivileged(new PrivilegedAction<URL>() {
@Override
public URL run() {
if (!e.hasMoreElements()) {
return null;
}
return (URL) e.nextElement();
}
}, DynamicURLClassLoader.this.acc);
if (u == null) {
break;
}
this.url = DynamicURLClassLoader.this.ucp.checkURL(u);
}
while (this.url == null);
return this.url != null;
}
@Override
public URL nextElement() {
if (!next()) {
throw new NoSuchElementException();
}
final URL u = this.url;
this.url = null;
return u;
}
@Override
public boolean hasMoreElements() {
return next();
}
};
}
/**
* Returns the permissions for the given codesource object.
* The implementation of this method first calls super.getPermissions
* and then adds permissions based on the URL of the codesource.
*
* <p>If the protocol is "file"
* and the path specifies a file, then permission to read that
* file is granted. If protocol is "file" and the path is
* a directory, permission is granted to read all files
* and (recursively) all files and subdirectories contained in
* that directory.
*
* <p>If the protocol is not "file", then
* to connect to and accept connections from the URL's host is granted.
* @param codesource the codesource
* @return the permissions granted to the codesource
*/
@Override
protected PermissionCollection getPermissions(CodeSource codesource) {
final PermissionCollection perms = super.getPermissions(codesource);
final URL url = codesource.getLocation();
Permission permission;
URLConnection urlConnection;
try {
urlConnection = url.openConnection();
permission = urlConnection.getPermission();
} catch (IOException ioe) {
permission = null;
urlConnection = null;
}
if ((permission != null) && (permission instanceof FilePermission)) {
// if the permission has a separator char on the end,
// it means the codebase is a directory, and we need
// to add an additional permission to read recursively
String path = permission.getName();
if (path.endsWith(File.separator)) {
path += "-"; //$NON-NLS-1$
permission = new FilePermission(path, sun.security.util.SecurityConstants.FILE_READ_ACTION);
}
} else if ((permission == null) && (URISchemeType.FILE.isURL(url))) {
String path = url.getFile().replace('/', File.separatorChar);
path = sun.net.www.ParseUtil.decode(path);
if (path.endsWith(File.separator)) {
path += "-"; //$NON-NLS-1$
}
permission = new FilePermission(path, sun.security.util.SecurityConstants.FILE_READ_ACTION);
} else {
URL locUrl = url;
if (urlConnection instanceof JarURLConnection) {
locUrl = ((JarURLConnection) urlConnection).getJarFileURL();
}
String host = locUrl.getHost();
if (host == null) {
host = "localhost"; //$NON-NLS-1$
}
permission = new SocketPermission(host,
sun.security.util.SecurityConstants.SOCKET_CONNECT_ACCEPT_ACTION);
}
// make sure the person that created this class loader
// would have this permission
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
final Permission fp = permission;
AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() throws SecurityException {
sm.checkPermission(fp);
return null;
}
}, this.acc);
}
perms.add(permission);
return perms;
}
/**
* Creates a new instance of DynamicURLClassLoader for the specified
* URLs and parent class loader. If a security manager is
* installed, the <code>loadClass</code> method of the URLClassLoader
* returned by this method will invoke the
* <code>SecurityManager.checkPackageAccess</code> method before
* loading the class.
*
* @param parent the parent class loader for delegation
* @param urls the URLs to search for classes and resources
* @return the resulting class loader
*/
@Pure
public static DynamicURLClassLoader newInstance(final ClassLoader parent, final URL... urls) {
// Save the caller's context
final AccessControlContext acc = AccessController.getContext();
// Need a privileged block to create the class loader
return AccessController.doPrivileged(new PrivilegedAction<DynamicURLClassLoader>() {
@Override
public DynamicURLClassLoader run() {
// Now set the context on the loader using the one we saved,
// not the one inside the privileged block...
return new FactoryDynamicURLClassLoader(parent, acc, urls);
}
});
}
/**
* Merge the specified URLs to the current classpath.
*/
private static URL[] mergeClassPath(URL... urls) {
final String path = System.getProperty("java.class.path"); //$NON-NLS-1$
final String separator = System.getProperty("path.separator"); //$NON-NLS-1$
final String[] parts = path.split(Pattern.quote(separator));
final URL[] u = new URL[parts.length + urls.length];
for (int i = 0; i < parts.length; ++i) {
try {
u[i] = new File(parts[i]).toURI().toURL();
} catch (MalformedURLException exception) {
// ignore exception
}
}
System.arraycopy(urls, 0, u, parts.length, urls.length);
return u;
}
/** This class loader permits to load classes from a set of classpaths.
* @author $Author: sgalland$
* @version $FullVersion$
* @mavengroupid $GroupId$
* @mavenartifactid $ArtifactId$
*/
protected static final class FactoryDynamicURLClassLoader extends DynamicURLClassLoader {
/**
* @param parent is the parent class loader.
* @param acc is the accessible context.
* @param urls is the list of urls to insert inside the class loading path.
*/
protected FactoryDynamicURLClassLoader(ClassLoader parent, AccessControlContext acc, URL... urls) {
super(parent, acc, urls);
}
@Override
public synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// First check if we have permission to access the package. This
// should go away once we've added support for exported packages.
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
final int i = name.lastIndexOf('.');
if (i != -1) {
sm.checkPackageAccess(name.substring(0, i));
}
}
return super.loadClass(name, resolve);
}
}
}