com.sun.enterprise.loader.ASURLClassLoader Maven / Gradle / Ivy
Show all versions of payara-micro Show documentation
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 2006-2013 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
* or packager/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at packager/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
// Portions Copyright [2016-2023] [Payara Foundation and/or its affiliates]
package com.sun.enterprise.loader;
import com.sun.appserv.server.util.PreprocessorUtil;
import com.sun.enterprise.security.integration.DDPermissionsLoader;
import com.sun.enterprise.security.integration.PermsHolder;
import com.sun.enterprise.util.CULoggerInfo;
import com.sun.enterprise.util.i18n.StringManager;
import org.glassfish.api.deployment.InstrumentableClassLoader;
import org.glassfish.hk2.api.PreDestroy;
import java.io.*;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import org.glassfish.hk2.utilities.CleanerFactory;
import java.net.*;
import java.nio.file.Path;
import java.security.*;
import java.security.cert.Certificate;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Predicate;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import org.glassfish.common.util.InstanceCounter;
/**
* Class loader used by the ejbs of an application or stand alone module.
*
* This class loader also keeps cache of not found classes and resources.
*
*
* @author Nazrul Islam
* @author Kenneth Saks
* @author Sivakumar Thyagarajan
* @since JDK 1.4
*/
public class ASURLClassLoader extends CurrentBeforeParentClassLoader
implements JasperAdapter, InstrumentableClassLoader, PreDestroy, DDPermissionsLoader {
/*
NOTE: various variables are 'final' to enjoy the JVM thread visibility guaranteed for 'final'.
These variables cannot be nulled out because of this, but the contents are cleared (for Map/Vector).
This is actually a hidden benefit, because there are places in the code where these variables
are being used but they could be nulled out while being used.
Another benefit is that there is no synchronization needed to get the Map/Vector/List itself.
*/
/** logger for this class */
private static final Logger _logger = CULoggerInfo.getLogger();
/*
list of url entries of this class loader. Using LinkedHashSet instead of original ArrayList
for faster search.
*/
private final Set urlSet = Collections.synchronizedSet(new LinkedHashSet<>());
/** cache of not found resources */
private final Map notFoundResources = new ConcurrentHashMap<>();
/** cache of not found classes */
private final Map notFoundClasses = new ConcurrentHashMap<>();
/**
State flag to track whether this instance has been shut off.
Note: 'volatile' *does not by itself eliminate a race condition* similar to null-check idiom bug.
*/
private volatile boolean doneCalled = false;
/**
snapshot of classloader state at the time done was called.
Must be 'volatile'; not all access is within 'synchronized' eg it is used in toString().
*/
private volatile String doneSnapshot;
private boolean mustLoadInThisStackFrame;
/** streams opened by this loader */
private final List streams = new CopyOnWriteArrayList<>();
private final ArrayList transformers = new ArrayList<>(1);
private static final StringManager sm = StringManager.getManager(ASURLClassLoader.class);
//holder for declared and ee permissions
private final PermsHolder permissionsHolder;
private final InstanceCounter instanceCounter = new InstanceCounter(this);
/**
* Constructor.
*/
public ASURLClassLoader() {
super(new URL[0]);
permissionsHolder = new PermsHolder();
if (_logger.isLoggable(Level.FINE)) {
_logger.log(Level.FINE, "ClassLoader: {0} is getting created.", this);
}
}
/**
* Constructor.
*
* @param parent parent class loader
*/
public ASURLClassLoader(ClassLoader parent) {
super(new URL[0], parent);
permissionsHolder = new PermsHolder();
}
public boolean isDone() {
// method need not by 'synchronized' because 'doneCalled' is 'volatile'.
return doneCalled;
}
@Override
public void preDestroy() {
done();
}
/**
* This method should be called to free up the resources.
* It helps garbage collection.
*
* Must be synchronized for:
(a) visibility of variables
(b) race condition while checking 'doneCalled'
(c) only one caller should close the zip files,
(d) done should occur only once and set the flag when done.
(e) shoudl not return 'true' when a previous thread might still
be in the process of executing the method.
*/
public void done() {
// This works because 'doneCalled' is 'volatile'
if( doneCalled ) {
return;
}
// the above optimized check for 'doneCalled=true' is a race condition.
// The lock must now be acquired, and 'doneCalled' rechecked.
synchronized(this) {
if( doneCalled ) {
return;
}
// Capture the fact that the classloader is now effectively disabled.
// First create a snapshot of our state. This should be called
// before setting doneCalled = true.
doneSnapshot = "ASURLClassLoader.done() called ON " + this.toString()
+ "\n AT " + new Date() +
" \n BY :" + Arrays.toString(Thread.currentThread().getStackTrace());
// Presumably OK to set this flag now while the rest of the cleanup proceeds,
// because we've taken the snapshot.
doneCalled = true;
// closes the jar handles and sets the url entries to null
for (URLEntry u : this.urlSet) {
u.close();
}
closeOpenStreams();
// clears out the tables
// Clear all values. Because fields are 'final' (for thread safety), cannot null them
this.urlSet.clear();
this.notFoundResources.clear();
this.notFoundClasses.clear();
}
}
/**
* Adds a URL to the search list, based on the specified File.
*
* This variant of the method makes sure that the URL is valid, in particular
* encoding special characters (such as blanks) in the file path.
* @param file the File to use in creating the URL
* @throws IOException in case of errors converting the file to a URL
*/
public void appendURL(File file) throws IOException {
try {
appendURL(file.toURI().toURL());
} catch (MalformedURLException mue) {
_logger.log(Level.SEVERE,
CULoggerInfo.getString(CULoggerInfo.badUrlEntry, file.toURI()),
mue);
throw new IOException(mue);
}
}
/**
* 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
*/
@Override
public void addURL(URL url) {
appendURL(url);
}
/**
* Add a url to the list of urls we search for a class's bytecodes.
*
* @param url url to be added
*/
public synchronized void appendURL(URL url) {
try {
if (url == null) {
_logger.log(Level.INFO, CULoggerInfo.missingURLEntry);
return;
}
URLEntry entry = new URLEntry(url);
if ( !urlSet.contains(entry) ) {
// adds the url entry to the list
this.urlSet.add(entry);
if (entry.isJar) {
// checks the manifest if a jar
checkManifest(entry.zip, entry.file);
}
} else {
_logger.log(Level.FINE, "[ASURLClassLoader] Ignoring duplicate URL: {0}", url);
/*
*Clean up the unused entry or it could hold open a jar file.
*/
if (entry.zip != null) {
try {
entry.zip.reallyClose();
} catch (IOException ioe) {
_logger.log(Level.INFO,
CULoggerInfo.getString(CULoggerInfo.exceptionClosingDupUrlEntry, url),
ioe);
}
}
}
// clears the "not found" cache since we are adding a new url
clearNotFoundCaches();
} catch (IOException ioe) {
_logger.log(Level.SEVERE,
CULoggerInfo.getString(CULoggerInfo.badUrlEntry, url),
ioe);
}
}
/**
* Returns the urls of this class loader.
*
* Method is 'synchronized' to avoid the thread-unsafe null-check idiom idiom, also
* protects the caller from simultaneous changes while iterating,
* by returning a URL[] (copy) rather than the original. Also protects against
* changes to 'urlSet' while iterating over it.
*
* @return the urls of this class loader or an empty array
*/
@Override
public synchronized URL[] getURLs() {
return this.urlSet.stream().map(u -> u.source).toArray(URL[]::new);
}
/**
* Returns all the "file" protocol resources of this ASURLClassLoader,
* concatenated to a classpath string.
*
* Notice that this method is called by the setClassPath() method of
* org.apache.catalina.loader.WebappLoader, since this ASURLClassLoader does
* not extend off of URLClassLoader.
*
* @return Classpath string containing all the "file" protocol resources
* of this ASURLClassLoader
*/
public String getClasspath() {
URL[] urls = getURLs();
if (urls != null) {
List files = Arrays.stream(urls)
.filter(url -> url.getProtocol().equals("file"))
.map(URL::getFile)
.collect(Collectors.toList());
if (!files.isEmpty()) {
return String.join(File.pathSeparator, files);
}
}
return null;
}
/**
*Refreshes the memory of the class loader. This involves clearing the
*not-found cahces and recreating the hash tables for the URLEntries that
*record the files accessible for each.
*
*Code that creates an ASURLClassLoader and then adds files to a directory
*that is in the loader's classpath should invoke this method after the new
*file(s) have been added in order to update the class loader's data
*structures which optimize class and resource searches.
*/
public synchronized void refresh() {
clearNotFoundCaches();
}
@Override
public void addTransformer(ClassFileTransformer transformer) {
transformers.add(transformer);
}
/**
* To be used when loading classes that are absolutely necessary for tha app
* to work, ignoring expensive negative lookups.
*
* Since DirWatched is being used, there is a possibility of a race condition
* when OS doesn't deliver file creation events fast enough.
* This method will disable DirWatcher for lookups.
* @param loader instance which should be
* @return AutoCloseable to be used with try-with-resources
* so the value gets reset when stack frame is existed
*/
public static AutoCloseable mustLoadFrom(ClassLoader loader) {
if (loader instanceof ASURLClassLoader) {
ASURLClassLoader self = (ASURLClassLoader)loader;
return self.mustLoad();
} else {
return () -> {};
}
}
private AutoCloseable mustLoad() {
mustLoadInThisStackFrame = true;
return () -> mustLoadInThisStackFrame = false;
}
/**
* Create a new instance of a sibling classloader
* @return a new instance of a class loader that has the same visibility
* as this class loader
*/
@Override
public ClassLoader copy() {
final ASURLClassLoader copyFrom = this;
return AccessController.doPrivileged((PrivilegedAction) () -> {
// privileged code goes here, for example:
return new DelegatingClassLoader(copyFrom);
});
}
/**
*Erases the memory of classes and resources that have been searched for
*but not found.
*/
private void clearNotFoundCaches() {
this.notFoundResources.clear();
this.notFoundClasses.clear();
}
/**
* Internal implementation of find resource.
*
* @param res url resource entry
* @param name name of the resource
*/
private URL findResource0(final URLEntry res,
final String name) {
// End for -- each URL in classpath.
return AccessController.doPrivileged((PrivilegedAction) () -> {
if (res.isJar) {
try {
JarEntry jarEntry = res.zip.getJarEntry(name);
if (jarEntry != null) {
/*
*Use a custom URL with a special stream handler to
*prevent the JDK's JarURLConnection caching from
*locking the jar file until JVM exit.
*/
InternalURLStreamHandler handler = new InternalURLStreamHandler(res, name);
// Create a new sub URL from the resource URL (i.e. res.source). To avoid double encoding
// (see https://glassfish.dev.java.net/issues/show_bug.cgi?id=13045)
// use URL constructor instead of first creating a URI and then calling toURL().
// If the resource URL is not properly encoded, that's not our problem.
// Whoever has supplied the resource URL is at fault.
URL ret = new URL("jar", null /* host */, -1 /* port */, res.source + "!/" + name, handler);
handler.tieUrl(ret);
return ret;
}
} catch (Throwable thr) {
_logger.log(Level.INFO, CULoggerInfo.exceptionInASURLClassLoader, thr);
}
} else { // directory
try {
File resourceFile =
new File(res.file.getCanonicalPath()
+ File.separator + name);
if (resourceFile.exists()) {
// If we make it this far,
// the resource is in the directory.
return resourceFile.toURI().toURL();
}
} catch (IOException e) {
_logger.log(Level.INFO, CULoggerInfo.exceptionInASURLClassLoader, e);
}
}
return null;
});
}
@Override
public URL findResource(String name) {
// quick quick that relies on 'doneCalled' being 'volatile'
if( doneCalled ) {
_logger.log(Level.WARNING,
CULoggerInfo.getString(CULoggerInfo.findResourceAfterDone, name, this.toString()),
new Throwable());
return null;
}
// This code is dubious, because it iterates over items that could
// be changing via another thread. It appears that since 'urlSet' cannot shrink
// that the iteration at least won't go out of bounds. And it's probably OK
// if more than one thread adds the same resource to 'notFoundResources'.
//
// HOWEVER, there is still a race condition from the check for 'doneCalled' above.
// That's OK for 'notFoundResources', but it could lead to an ArrayIndexOutOfBounds
// excpetion for 'urlSet', should the set be cleared while looping.
// resource is in the not found list
String nf = notFoundResources.get(name);
if (nf != null && nf.equals(name) ) {
return null;
}
synchronized(this) {
for (final URLEntry u : this.urlSet) {
if (!u.hasItem(name, this)) {
continue;
}
final URL url = findResource0(u, name);
if (url != null) return url;
}
}
// add resource to the not found list
notFoundResources.put(name, name);
return null;
}
/**
* Returns an enumeration of java.net.URL objects
* representing all the resources with the given name.
*
* This method is synchronized to avoid (a) race condition checking 'doneCalled',
* (b) changes to contents or length of 'resourcesList' and/or 'notFoundResources' while iterating
* over them, (c) thread visibility to all of the above.
*/
private boolean warnedOnce = false;
@Override
public synchronized Enumeration findResources(String name) {
if( doneCalled ) {
//PAYARA-588
if ( warnedOnce ) {
_logger.log(Level.FINE, CULoggerInfo.doneAlreadyCalled,
new Object[] { name, doneSnapshot });
} else {
_logger.log(Level.WARNING, CULoggerInfo.doneAlreadyCalled,
new Object[] { name, doneSnapshot });
warnedOnce = true;
}
// return an empty enumeration instead of null. See issue #13096
return Collections.emptyEnumeration();
}
List resourcesList = new ArrayList<>();
// resource is in the not found list
final String nf = notFoundResources.get(name);
if (nf != null && nf.equals(name) ) {
return Collections.enumeration (resourcesList);
}
for (final URLEntry urlEntry : this.urlSet) {
final URL url = findResource0(urlEntry, name);
if (url != null) {
resourcesList.add(url);
}
}
if (resourcesList.isEmpty()) {
// add resource to the not found list
notFoundResources.put(name, name);
}
return Collections.enumeration(resourcesList);
}
/**
* Checks the manifest of the given jar file.
*
* @param jar the jar file that may contain manifest class path
* @param file file pointer to the jar
*
* @throws IOException if an i/o error
*/
private void checkManifest(JarFile jar, File file) throws IOException {
if ( (jar == null) || (file == null) ) return;
Manifest man = jar.getManifest();
if (man == null) return;
synchronized (this) {
String cp = man.getMainAttributes().getValue(
Attributes.Name.CLASS_PATH);
if (cp == null) return;
StringTokenizer st = new StringTokenizer(cp, " ");
while (st.hasMoreTokens()) {
final String entry = st.nextToken();
final File newFile = new File(file.getParentFile(), entry);
// add to class path of this class loader
try {
if (newFile.exists()) {
appendURL(newFile);
}
} catch (MalformedURLException ex) {
_logger.log(Level.SEVERE, CULoggerInfo.exceptionInASURLClassLoader, ex);
}
}
}
}
/**
* Internal implementation of load class.
*
* @param res url resource entry
* @param entryName name of the class
*/
private byte[] loadClassData0(final URLEntry res, final String entryName) {
return AccessController.doPrivileged((PrivilegedAction) () -> {
try {
if (res.isJar) { // It is a jarfile..
JarFile zip = res.zip;
JarFile jar = new JarFile(res.file, false, JarFile.OPEN_READ,
Runtime.Version.parse(System.getProperty("java.version")));
JarEntry entry = jar.getJarEntry(entryName);
if (entry != null) {
byte[] classData = getClassData(zip.getInputStream(entry));
res.setProtectionDomain(ASURLClassLoader.this, entry.getCertificates());
return classData;
}
} else { // Its a directory....
String entryPath = entryName.replace('/', File.separatorChar);
File classFile = new File (res.file, entryPath);
if (classFile.exists()) {
try (InputStream classStream = new FileInputStream(classFile)) {
byte[] classData = getClassData(classStream);
res.setProtectionDomain(ASURLClassLoader.this, null);
return classData;
}
}
File multiVersionDir = new File(res.file, "META-INF/versions/");
if (multiVersionDir.exists()) {
File multiVersionClassFile = new File(multiVersionDir, entryPath);
if (multiVersionClassFile.exists()) {
try (InputStream classStream = new FileInputStream(multiVersionClassFile)) {
byte[] classData = getClassData(classStream);
res.setProtectionDomain(ASURLClassLoader.this, null);
return classData;
}
}
}
}
} catch (IOException ioe) {
_logger.log(Level.INFO, CULoggerInfo.exceptionInASURLClassLoader, ioe);
}
return null;
});
}
@Override
public void addEEPermissions(PermissionCollection eePc)
throws SecurityException {
// sm on
if (System.getSecurityManager() != null) {
System.getSecurityManager().checkSecurityAccess(
DDPermissionsLoader.SET_EE_POLICY);
permissionsHolder.setEEPermissions(eePc);
}
}
@Override
public void addDeclaredPermissions(PermissionCollection declaredPc) throws SecurityException {
if (System.getSecurityManager() != null) {
System.getSecurityManager().checkSecurityAccess(
DDPermissionsLoader.SET_EE_POLICY);
permissionsHolder.setDeclaredPermissions(declaredPc);
}
}
@Override
protected PermissionCollection getPermissions(CodeSource codeSource) {
PermissionCollection cachedPc =
permissionsHolder.getCachedPerms(codeSource);
if (cachedPc != null)
return cachedPc;
return permissionsHolder.getPermissions(
codeSource, super.getPermissions(codeSource));
}
/** THREAD SAFETY: what happens when more than one thread requests the same class
and thus works on the same classData? Or defines the same package? Maybe
the same work just gets done twice, and that's all.
CAUTION: this method might be overriden, and subclasses must be cautious (also)
about thread safety.
*/
@Override
protected Class findClass(String name) throws ClassNotFoundException {
ClassData classData = findClassData(name);
// Instruments the classes if the profiler's enabled
if (PreprocessorUtil.isPreprocessorEnabled()) {
// search thru the JARs for a file of the form java/lang/Object.class
final String entryName = name.replace('.', '/') + ".class";
classData.setClassBytes(PreprocessorUtil.processClass(entryName, classData.getClassBytes()));
}
// Define package information if necessary
int lastPackageSep = name.lastIndexOf('.');
if ( lastPackageSep != -1 ) {
String packageName = name.substring(0, lastPackageSep);
if( getPackage(packageName) == null ) {
try {
// There's a small chance that one of our parents
// could define the same package after getPackage
// returns null but before we call definePackage,
// since the parent classloader instances
// are not locked. So, just catch the exception
// that is thrown in that case and ignore it.
//
// It's unclear where we would get the info to
// set all spec and impl data for the package,
// so just use null. This is consistent will the
// JDK code that does the same.
definePackage(packageName, null, null, null,
null, null, null, null);
} catch(IllegalArgumentException iae) {
// duplicate attempt to define same package.
// safe to ignore.
_logger.log(Level.FINE, "duplicate package " +
"definition attempt for " + packageName, iae);
}
}
}
// Loop though the transformers here!!
try {
final ArrayList xformers = (ArrayList) transformers.clone();
for ( final ClassFileTransformer transformer : xformers) {
// see javadocs of transform().
// It expects class name as java/lang/Object
// as opposed to java.lang.Object
final String internalClassName = name.replace('.','/');
final byte[] transformedBytes = transformer.transform(this, internalClassName, null,
classData.pd, classData.getClassBytes());
if(transformedBytes!=null){ // null indicates no transformation
_logger.log(Level.INFO, CULoggerInfo.actuallyTransformed, name);
classData.setClassBytes(transformedBytes);
}
}
} catch (IllegalClassFormatException icfEx) {
throw new ClassNotFoundException(icfEx.toString(), icfEx);
}
try {
byte[] bytes = classData.getClassBytes();
return defineClass(name, bytes, 0, bytes.length, classData.pd);
} catch (UnsupportedClassVersionError ucve) {
throw new UnsupportedClassVersionError(
sm.getString("ejbClassLoader.unsupportedVersion", name,
System.getProperty("java.version")));
}
}
/**
* This method is responsible for locating the url from the class bytes
* have to be read and reading the bytes. It does not actually define
* the Class object.
*
* To preclude a race condition on checking 'doneCalled', as well as transient errors
* if done() is called while running, this method is 'synchronized'.
* @param name class name in java.lang.Object format
* @return class bytes as well protection domain information
* @throws ClassNotFoundException
*/
protected synchronized ClassData findClassData(String name) throws ClassNotFoundException {
if( doneCalled ) {
_logger.log(Level.WARNING,
CULoggerInfo.getString(CULoggerInfo.findClassAfterDone, name, this.toString()),
new Throwable());
throw new ClassNotFoundException(name);
}
String nf = notFoundClasses.get(name);
if (nf != null && nf.equals(name) ) {
throw new ClassNotFoundException(name);
}
// search through the JARs for a file of the form java/lang/Object.class
String entryName = name.replace('.', '/') + ".class";
for (URLEntry u : this.urlSet) {
if (!u.hasItem(entryName, this)) {
continue;
}
byte[] result = loadClassData0(u, entryName);
if (result != null) {
if (System.getSecurityManager() == null) {
return new ClassData(result, u.pd);
} else {
//recreate the pd to include the declared permissions
CodeSource cs = u.pd.getCodeSource();
PermissionCollection pc = this.getPermissions(cs);
ProtectionDomain pdWithPemissions =
new ProtectionDomain(u.pd.getCodeSource(), pc, u.pd.getClassLoader(), u.pd.getPrincipals());
return new ClassData(result, pdWithPemissions);
}
}
}
// add to the not found classes list
notFoundClasses.put(name, name);
throw new ClassNotFoundException(name);
}
/**
* Returns the byte array from the given input stream.
*
* @param istream input stream to the class or resource
*
* @throws IOException if an i/o error
*/
private byte[] getClassData(InputStream istream) throws IOException {
byte[] buf = new byte[4096];
ByteArrayOutputStream bout = new ByteArrayOutputStream();
try (BufferedInputStream bstream = new BufferedInputStream(istream)) {
int num;
while( (num = bstream.read(buf)) != -1) {
bout.write(buf, 0, num);
}
}
return bout.toByteArray();
}
protected String getClassLoaderName() {
return "ASURLClassLoader";
}
/**
* Returns a string representation of this class loader.
*
* @return a string representation of this class loader
*/
@Override
public String toString() {
StringBuilder buffer = new StringBuilder();
buffer.append(getClassLoaderName()).append(" : \n");
if( doneCalled ) {
buffer.append("doneCalled = true" + "\n");
String snapshot = doneSnapshot; // MUST use temp for thread safety; could go null after checking
if( snapshot != null ) {
buffer.append("doneSnapshot = ").append(snapshot);
}
} else {
buffer.append("urlSet = ").append(this.urlSet).append("\n");
buffer.append("doneCalled = false \n");
}
buffer.append(" Parent -> ").append(getParent()).append("\n");
return buffer.toString();
}
@Override
public InputStream getResourceAsStream(final String name) {
InputStream stream = super.getResourceAsStream(name);
/*
*Make sure not to wrap the stream if it already is a wrapper.
*/
if (stream != null) {
if (! (stream instanceof SentinelInputStream)) {
stream = new SentinelInputStream(stream);
}
}
return stream;
}
/**
* The JarFile objects loaded in the classloader may get exposed to the
* application code (e.g. EJBs) through calls of
* ((JarURLConnection) getResource().openConnection()).getJarFile().
*
* This class protects the jar file from being closed by such an application.
*
* @author fkieviet
*/
private static final class ProtectedJarFile extends JarFile {
/**
* Constructor
*
* @param file File
* @throws IOException from parent
*/
public ProtectedJarFile(File file) throws IOException {
super(file);
registerCloseEvent();
}
/**
* Do nothing
*
* @see java.util.zip.ZipFile#close()
*
* Byron sez: I wonder what's going on here?!? This looks quite weird.
* Why not just get rid of both reallyClose() and close() and finalize()
* -- and just rely on the superclass?
* At any rate I am not messing with it today, 1/9/2013. Mainly because
* maybe close() is called outside and we do NOT want it to really close?
* Here is what happens at finalize time:
* 1. ASURLClassLoader$ProtectedJarFile.finalize()
* 2. java.util.zip.ZipFile.finalize()
* 3. ASURLClassLoader$ProtectedJarFile.close()
* 4. dumps a WARNING log message
* 5. reallyClose()
* 6. java.util.zip.ZipFile.close()
* I
*/
@Override
public void close() {
// do nothing
}
/**
* Really close the jar file
*
* @throws IOException from parent
*/
public void reallyClose() throws IOException {
super.close();
}
public final void registerCloseEvent() {
CleanerFactory.create().register(this, () -> {
try {
reallyClose();
} catch (IOException ex) {
_logger.log(Level.WARNING, null, ex);
}
});
}
}
/**
* URL entry - keeps track of the url resources.
*/
protected static final class URLEntry {
/** the url, ensure thread visibility by making it 'final' */
final URL source;
/** file of the url,
ensure thread visibility by making it 'volatile' */
volatile File file = null;
/** jar file if url is a jar else null,
ensure thread visibility by making it 'volatile' */
volatile ProtectedJarFile zip = null;
/** true if url is a jar,
ensure thread visibility by making it 'volatile' */
volatile boolean isJar = false;
private Predicate presenceCheck;
/** ProtectionDomain with signers if jar is signed,
ensure thread visibility by making it 'volatile' */
volatile ProtectionDomain pd = null;
URLEntry(URL url) throws IOException {
source = url;
init();
}
void init() throws IOException {
try {
file = new File(source.toURI());
isJar = file.isFile();
if (isJar) {
zip = new ProtectedJarFile(file);
// negative lookups in jar are cheap
presenceCheck = (any) -> true;
} else {
Path path = file.toPath();
presenceCheck = (item) -> DirWatcher.hasItem(path, item);
DirWatcher.register(path);
}
} catch (URISyntaxException use) {
throw new IOException(use);
}
}
boolean hasItem(String item, ASURLClassLoader classLoader) {
return classLoader.mustLoadInThisStackFrame || presenceCheck.test(item);
}
/**
* Sets ProtectionDomain with CodeSource including Signers in
* Entry for use in call to defineClass.
* @param signers the array of signer certs or null
*/
public void setProtectionDomain (ClassLoader ejbClassLoader, Certificate[] signers) throws MalformedURLException {
if (pd == null) {
pd = new ProtectionDomain(new CodeSource(file.toURI().toURL(),signers),null, ejbClassLoader, null );
}
}
@Override
public String toString() {
return "URLEntry : " + source.toString();
}
/**
* Returns true if two URL entries has equal URLs.
*
* @param obj URLEntry to compare against
* @return true if both entry has equal URL
*/
@Override
public boolean equals(Object obj) {
boolean tf = false;
if (obj instanceof URLEntry) {
URLEntry e = (URLEntry) obj;
try {
//try comparing URIs
if (source.toURI().equals(e.source.toURI())) {
tf = true;
}
} catch (URISyntaxException e1) {
// We should never get here, because we call init() in the constructor and
// init() would have thrown an exception if the URL could not be converted to a valid URI.
assert(false);
throw new RuntimeException(e1);
}
}
return tf;
}
/**
* Since equals is overridden, we need to override hashCode as well.
*/
@Override
public int hashCode() {
try {
return source.toURI().hashCode();
} catch (URISyntaxException e) {
// We should never get here, because we call init() in the constructor and
// init() would have thrown an exception if the URL could not be converted to a valid URI.
assert(false);
throw new RuntimeException(e);
}
}
public void close() {
if (zip != null) {
try {
zip.reallyClose();
} catch (IOException ioe) {
_logger.log(Level.INFO,
CULoggerInfo.getString(CULoggerInfo.exceptionClosingURLEntry, source),
ioe);
}
}
if (!isJar) {
DirWatcher.unregister(file.toPath());
}
}
}
/**
*Closes any streams that remain open, logging a warning for each.
*
*This method should be invoked when the loader will no longer be used
*and the app will no longer explicitly close any streams it may have opened.
* Must be synchnronized to (a) avoid race condition checking 'streams'.
*/
private synchronized void closeOpenStreams() {
SentinelInputStream[] toClose = streams.toArray(new SentinelInputStream[0]);
for (SentinelInputStream s : toClose) {
s.closeWithWarning();
}
streams.clear();
}
/**
* Wraps all InputStreams returned by this class loader to report when
* a finalizer is run before the stream has been closed. This helps
* to identify where locked files could arise.
* @author vtsyganok
* @author tjquinn
*/
protected final class SentinelInputStream extends FilterInputStream {
private volatile boolean closed = false;
private volatile Throwable throwable;
/**
* Constructs new FilteredInputStream which reports InputStreams not closed properly.
* When the garbage collector runs the cleaner. If the stream is still open this class will
* report a stack trace showing where the stream was opened.
*
* @param in - InputStream to be wrapped
*/
protected SentinelInputStream(final InputStream in) {
super(in);
throwable = new Throwable();
getStreams().add(this);
registerStopEvent();
}
/**
* Closes underlying input stream.
*/
@Override
public void close() throws IOException {
_close();
}
/**
* Callback invoked by Garbage Collector. If underlying InputStream was not closed properly,
* the stack trace of the constructor will be logged!
*
* 'closed' is 'volatile', but it's a race condition to check it and how this code
* relates to _close() is unclear.
*/
public final void registerStopEvent() {
CleanerFactory.create().register(this, () -> closeWithWarning());
}
/**
* Returns the vector of open streams; creates it if needed.
*
* @return Vector holding open streams
*/
private List getStreams() {
return streams;
}
private synchronized void _close() throws IOException {
if ( closed ) {
return;
}
// race condition with above check, but should have no harmful effects
closed = true;
throwable = null;
getStreams().remove(this);
super.close();
}
private void closeWithWarning() {
if ( closed ) {
return;
}
// stores the internal Throwable as it will be set to null in the _close method
Throwable localThrowable = this.throwable;
try {
_close();
} catch (IOException ioe) {
_logger.log(Level.WARNING, CULoggerInfo.exceptionClosingStream, ioe);
}
report(localThrowable);
}
/**
* Report "left-overs"!
*/
private void report(Throwable localThrowable) {
_logger.log(Level.WARNING, CULoggerInfo.inputStreamFinalized, localThrowable);
}
}
/**
* To properly close streams obtained through URL.getResource().getStream():
* this opens the input stream on a JarFile that is already open as part
* of the classloader, and returns a sentinel stream on it.
*
* @author fkieviet
*/
private class InternalJarURLConnection extends JarURLConnection {
private final URLEntry mRes;
private final String mName;
/**
* Constructor
*
* @param url the URL that is a stream for
* @param res URLEntry
* @param name String
* @throws MalformedURLException from super class
*/
public InternalJarURLConnection(URL url, URLEntry res, String name)
throws MalformedURLException {
super(url);
mRes = res;
mName = name;
}
/**
* @see java.net.JarURLConnection#getJarFile()
*/
@Override
public JarFile getJarFile() throws IOException {
return mRes.zip;
}
/**
* @see java.net.URLConnection#connect()
*/
@Override
public void connect() throws IOException {
// Nothing
}
/**
* @see java.net.URLConnection#getInputStream()
*/
@Override
public InputStream getInputStream() throws IOException {
// When there is no entry name specified (this can happen for url like jar:file:///tmp/foo.jar!/),
// we must throw an IOException as that's the behavior of JarURLConnection as well.
if ("".equals(mName)) {
throw new IOException("no entry name specified");
}
ZipEntry entry = mRes.zip.getEntry(mName);
if (entry == null) {
throw new IOException("no entry called " + mName + " found in " + mRes.source);
}
return new SentinelInputStream(mRes.zip.getInputStream(entry));
}
}
/**
* To properly close streams obtained through URL.getResource().getStream():
* an instance of this class is instantiated for each and every URL object
* created by this classloader. It provides a custom JarURLConnection
* (InternalJarURLConnection) so that the stream can be obtained from an already
* open jar file.
*
* @author fkieviet
*/
private class InternalURLStreamHandler extends URLStreamHandler {
/** must be 'volatile' for thread visibility */
private volatile URL mURL;
private final URLEntry mRes;
/**
* Constructor
*
* @param res URLEntry
* @param name String
*/
public InternalURLStreamHandler(URLEntry res, String name) {
mRes = res;
}
/**
* @see java.net.URLStreamHandler#openConnection(java.net.URL)
*/
@Override
protected URLConnection openConnection(final URL u) throws IOException {
String path = u.getPath();
int separator = path.lastIndexOf('!');
assert(separator != -1); // we deal with jar urls only
try {
URI jarFileURI = new URI(path.substring(0, separator));
if (!jarFileURI.equals(mRes.file.toURI())) {
throw new IOException("Cannot open a foreign URL; this.url=" + mURL
+ "; foreign.url=" + u);
}
String entryName = path.substring(separator+1);
if (entryName != null) {
assert (entryName.startsWith("/"));
entryName = entryName.substring(1);
}
return new InternalJarURLConnection(u, mRes, entryName);
} catch (URISyntaxException e) {
throw new IOException(e);
}
}
/**
* Ties the URL that this handler is associated with to the handler, so
* that it can be asserted that somehow no other URLs are mangled in (this
* is theoretically impossible)
*
* @param url URL
*/
public void tieUrl(URL url) {
// is it OK to call this twice and whack the variable a second time?
if ( mURL != null )
{
throw new IllegalStateException("Setting the URL more than once not allowed" );
}
mURL = url;
}
}
/**
* This class is used as return value of findClassIntenal method to return
* both class bytes and protection domain.
*/
private static final class ClassData {
// Byron Sez: making classBytes volatile is pointless. That just makes
// the reference to the array volatile. The byte contents are NOT volatile
// I solved this low-level FB issue by:
// 1. don't use the field directly outside of this inner class
// 2. add a getter/setter
// 3. make both the setter and getter synchronized.
private byte[] classBytes;
/** must be 'final' to ensure thread visibility */
private final ProtectionDomain pd;
ClassData(byte[] classBytes, ProtectionDomain pd) {
this.classBytes = classBytes;
this.pd = pd;
}
private synchronized byte[] getClassBytes() {
return classBytes;
}
private synchronized void setClassBytes(byte[] newBytes) {
classBytes = newBytes;
}
}
/**
* This class loader only provides a new class loading namespace
* so that persistence provider can load classes in that separate
* namespace while scanning annotations.
* This class loader delegates all stream handling (i.e. reading
* actual class/resource data) operations to the application class loader.
* It only defines the Class using the byte codes.
* Motivation behind this class is discussed at
* https://glassfish.dev.java.net/issues/show_bug.cgi?id=237.
*/
private static final class DelegatingClassLoader extends SecureClassLoader {
/**
* The application class loader which is used to read class data.
* Made 'final' to ensure thread visibility.
*/
private final ASURLClassLoader delegate;
/**
* Create a new instance.
* @param applicationCL is the original class loader associated
* with this application. The new class loader uses it to delegate
* stream handling operations. The new class loader also uses
* applicationCL's parent as its own parent.
*/
DelegatingClassLoader(ASURLClassLoader applicationCL) {
super(applicationCL.getParent()); // normal class loading delegation
this.delegate = applicationCL;
}
/**
* This method uses the delegate to use class bytes and then defines
* the class using this class loader
*/
@Override
protected Class findClass(String name) throws ClassNotFoundException {
ClassData classData = delegate.findClassData(name);
// Define package information if necessary
int lastPackageSep = name.lastIndexOf('.');
if ( lastPackageSep != -1 ) {
String packageName = name.substring(0, lastPackageSep);
if( getPackage(packageName) == null ) {
try {
// There's a small chance that one of our parents
// could define the same package after getPackage
// returns null but before we call definePackage,
// since the parent classloader instances
// are not locked. So, just catch the exception
// that is thrown in that case and ignore it.
//
// It's unclear where we would get the info to
// set all spec and impl data for the package,
// so just use null. This is consistent will the
// JDK code that does the same.
definePackage(packageName, null, null, null,
null, null, null, null);
} catch(IllegalArgumentException iae) {
// duplicate attempt to define same package.
// safe to ignore.
_logger.log(Level.FINE, "duplicate package " +
"definition attempt for " + packageName, iae);
}
}
}
Class clazz = null;
try {
final byte[] bytes = classData.getClassBytes();
clazz = defineClass(name, bytes, 0, bytes.length, classData.pd);
return clazz;
} catch (UnsupportedClassVersionError ucve) {
throw new UnsupportedClassVersionError(
sm.getString("ejbClassLoader.unsupportedVersion", name,
System.getProperty("java.version")));
}
}
@Override
protected URL findResource(String name) {
return delegate.findResource(name);
}
@Override
protected Enumeration findResources(String name) throws IOException {
return delegate.findResources(name);
}
}
}