blob: aab6e14f950b2432162d22bf0f218fc96e58dac7 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. 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.
*/
package org.apache.jackrabbit.classloader;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import javax.jcr.Property;
import javax.jcr.RepositoryException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.jackrabbit.net.URLFactory;
/**
* The <code>ArchiveClassPathEntry</code> implements the {@link ClassPathEntry}
* abstract class with support for archives containing classes and other
* resources. The path used to construct the instance is the path of an item
* resolving to a property containing the jar archive to access.
*
* @author Felix Meschberger
*/
class ArchiveClassPathEntry extends ClassPathEntry {
/** Default logger */
private static final Log log =
LogFactory.getLog(ArchiveClassPathEntry.class);
/** The property containing the archive */
private final Property prop;
/**
* Cache all entries in the archive for faster decision on whether such
* an entry is contained.
*/
private Map entryMap;
/**
* The JAR file manifest. Set on demand by the {@link #getManifest()}
* method.
*/
private Manifest jarManifest;
/**
* Flag to indicate, whether the {@link #jarManifest} has already been read
* from the archive. This field is used and set by the
* {@link #getManifest()} to decide, whether to try to read the manifest.
*/
private boolean jarManifestRead;
/**
* Creates an instance of the <code>ArchiveClassPathEntry</code> class.
*
* @param prop The <code>Property</code> containing the archive and
* the session used to access the repository.
* @param path The original class path entry leading to the creation of
* this instance. This is not necessairily the same path as the
* properties path if the property was found through the primary
* item chain.
*
* @throws RepositoryException If an error occurrs retrieving the session
* from the property.
*/
ArchiveClassPathEntry(Property prop, String path) throws RepositoryException {
super(prop.getSession(), path);
this.prop = prop;
}
/**
* Clones the indicated <code>ArchiveClassPathEntry</code> object by
* taking over its path, session and property.
*
* @param base The base <code>ArchiveClassPath</code> entry to clone.
*
* @see ClassPathEntry#ClassPathEntry(ClassPathEntry)
*/
protected ArchiveClassPathEntry(ArchiveClassPathEntry base) {
super(base);
this.prop = base.prop;
}
/**
* Returns the <code>Property</code> containing the JAR file of this
* archive class path entry.
*/
protected Property getProperty() {
return prop;
}
/**
* Returns a {@link ClassLoaderResource} for the named resource if it
* can be found in the archive identified by the path given at
* construction time. Note that if the archive property would exist but is
* not readable by the current session, no resource is returned.
* <p>
* This method accesses the archive through an <code>InputStream</code>
* retrievedfrom the property. This <code>InputStream</code> is closed before
* returning to the caller to release the resources behind the stream
* such that it might be updated, etc. For this reason the resource
* instance returned will again open an <code>InputStream</code> on the
* archive property to access the resource. Users of the resource
* <code>InputStream</code> are encouraged to close the stream when no
* longer used to prevent lockups in the Repository.
*
* @param name The name of the resource to return. If the resource would
* be a class the name must already be modified to denote a valid
* path, that is dots replaced by slashes and the <code>.class</code>
* extension attached.
*
* @return The {@link ClassLoaderResource} identified by the name or
* <code>null</code> if no resource is found for that name.
*/
public ClassLoaderResource getResource(final String name) {
JarInputStream zins = null;
try {
// get the archive and try to find the entry
zins = getJarInputStream(prop);
JarEntry entry = findEntry(zins, name);
// if found create the resource to return
if (entry != null) {
return new ArchiveClassPathResource(this, entry);
}
log.debug("getResource: resource " + name + " not found"
+ " in archive " + path);
} catch (IOException ioe) {
log.warn("getResource: problem accessing the archive " + path
+ " for " + name + ": " + ioe.toString());
} catch (RepositoryException re) {
log.warn("getResource: problem accessing the archive " + path
+ " for " + name + ": " + re.toString());
} finally {
// make sure streams are closed at the end
if (zins != null) {
try {
zins.close();
} catch (IOException ignore) {
}
}
}
// invariant : not found or problem accessing the archive
return null;
}
/**
* Returns a <code>ClassPathEntry</code> with the same configuration as
* this <code>ClassPathEntry</code>.
* <p>
* The <code>ArchiveClassPathEntry</code> class has internal state.
* Therefore a new instance is created from the unmodifiable configuration
* of this instance.
*/
ClassPathEntry copy() {
return new ArchiveClassPathEntry(this);
}
/**
* Returns a JAR URL with no entry as the base URL of this class path entry.
*/
public URL toURL() {
if (baseURL == null) {
try {
baseURL = URLFactory.createJarURL(session, path, null);
} catch (MalformedURLException mue) {
log.warn("Problem creating baseURI for " + path, mue);
}
}
return baseURL;
}
//----------- internal helper to find the entry ------------------------
/**
* Returns a JAR URL to access the named resource within the archive
* underlying this class path entry. This is a helper method for the
* {@link ClassLoaderResource} instance returned by
* {@link #getResource(String)} method.
* <p>
* This method does not check, whether the named entry actually exists in
* the underlying archive.
*
* @param name The name of the resource for which to create the JAR URL.
*/
protected URL getURL(String name) {
try {
return URLFactory.createJarURL(session, path, name);
} catch (MalformedURLException mue) {
log.error("getURL: Cannot create URL for " + name, mue);
}
return null;
}
/**
* Returns an URL to access the underlying archive itself of this class
* path entry. The URL returned may be used as the code source for Java
* securtiy protection domains. This is a helper method for the
* {@link ClassLoaderResource} instance returned by
* {@link #getResource(String)} method.
*
* @return The URL to access the underlying archive.
*/
protected URL getCodeSourceURL() {
try {
return URLFactory.createURL(session, path);
} catch (MalformedURLException mue) {
log.warn("getCodeSourceURL: Cannot getURL" + " for " + path, mue);
}
return null;
}
/**
* Returns a <code>JarInputStream</code> from the property.
*
* @param property The <code>Property</code> containing the archive to
* access.
*
* @return A valid <code>JarInputStream</code>.
*
* @throws RepositoryException If an <code>InputStream</code> cannot be
* retrieved from the property.
* @throws IOException If the <code>JarInputStream</code> cannot be
* created.
*/
static JarInputStream getJarInputStream(Property property)
throws RepositoryException, IOException {
return new JarInputStream(property.getStream());
}
/**
* Returns the <code>Manifest</code> object of the JAR archive file
* underlying this archive class path entry. If no manifest exists in the
* JAR file or if the archive is not a JAR file at - e.g. a plain ZIP
* file - this method returns <code>null</code>. If an error occurrs
* trying to access the manifest, <code>null</code> is also returned. Later
* calls to this method, will not try again to read the manifest file,
* though.
* <p>
* This method is synchronized to prevent two threads from trying to access
* the manifest at the same time, which might result in false negative
* returned.
*
* @return The manifest contained in the underlying JAR file or
* <code>null</code> if none exists or an error occurrs trying to
* load the manifest.
*/
protected synchronized Manifest getManifest() {
if (jarManifest == null && !jarManifestRead) {
// immediately mark the manifest read, to prevent repeated read
// in the case of missing manifest
jarManifestRead = true;
JarInputStream zipIns = null;
try {
zipIns = new JarInputStream(prop.getStream());
jarManifest = zipIns.getManifest();
} catch (RepositoryException re) {
log.warn("Cannot access JAR file " + getPath(), re);
} catch (IOException ioe) {
log.warn("Cannot access manifest of JAR file " + getPath(), ioe);
} finally {
if (zipIns != null) {
try {
zipIns.close();
} catch (IOException ignore) {
}
}
}
}
return jarManifest;
}
/**
* Returns the <code>JarEntry</code> for the path from the
* <code>JarInputStream</code> or <code>null</code> if the path cannot
* be found in the archive.
*
* @param zins The <code>JarInputStream</code> to search in.
* @param path The path of the <code>JarEntry</code> to return.
*
* @return The <code>JarEntry</code> for the path or <code>null</code>
* if no such entry can be found.
*
* @throws IOException if a problem occurrs reading from the stream.
*/
JarEntry findEntry(JarInputStream zins, String path)
throws IOException {
if (entryMap == null) {
// make sure to not build the list twice
synchronized (this) {
/**
* make sure, we still need to build the list. this
* implementation surely does not cure all problems of the
* double-checked-locking problem, but it surely remmedies
* the main problem where the reference is already written
* to the field before the constructor has finished. Also
* this only assigns the field when the contents has been
* filled.
*/
if (entryMap == null) {
// prepare an empty entry map to be filled
Map tmpEntryMap = new HashMap();
try {
// build the name-to-index map
log.debug("findEntry: Building map while searching");
JarEntry result = null;
JarEntry entry = zins.getNextJarEntry();
for (int i=0; entry != null; i++) {
// add the entry to the map
String name = entry.getName();
Integer entryNumO = new Integer(i);
tmpEntryMap.put(name, entryNumO);
log.debug("findEntry: Entry " + name + " ==> " +
entryNumO);
// if we found our entry, keep it to be returned later
if (result == null && path.equals(name)) {
log.debug("findEntry: Found the entry, " +
"continue indexing");
result = entry;
}
// on to the next entry
entry = zins.getNextJarEntry();
}
// invariant: path has the entry found or null
// return what we found
log.debug("findEntry: Indexing complete, " +
"returning " + result);
return result;
} finally {
/**
* assign the finished tmp entryMap to the field now.
* theoretically, this may still be null, which
* is no issue because it will be tried to be
* rebuilt - over and over again, though - by the
* next call to findEntry.
* in the case of build problems, the map be empty
* in which case it will not be rebuilt, which is
* ok, too, given that reading will still yield
* problems.
*/
entryMap = tmpEntryMap;
}
}
}
}
// invariant: entryMap is not null, but might be empty
// ( in case of problems creating the tmpEntryMap above, e.g.
// OutOfMemoryError, the entryMap might be null, but then we
// are thrown out of the method any way ... this is no issue
// here )
// map exists, lets try to get via number
Number entryNumO = (Number) entryMap.get(path);
if (entryNumO == null) {
log.debug("findEntry: This archive does not contain " + path);
return null;
}
// find the indexed entry
log.debug("findEntry: " + path + " is entry #" + entryNumO);
int entryNum = entryNumO.intValue();
JarEntry entry = zins.getNextJarEntry();
while (entryNum > 0 && entry != null) {
entry = zins.getNextJarEntry();
entryNum--;
}
return entry;
}
/**
* The <code>ArchiveClassPathResource</code> extends the
* {@link ClassLoaderResource} with support to extract resources from a
* JAR or ZIP archive.
*
* @author Felix Meschberger
*/
private static class ArchiveClassPathResource extends ClassLoaderResource {
/**
* The JAR/ZIP file entry providing the name, size and modification
* time information.
*/
private final JarEntry jarEntry;
/**
* Creates an instance of this resource for the given
* {@link ArchiveClassPathEntry} and JAR/ZIP file entry.
*
* @param pathEntry The {@link ArchiveClassPathEntry} from which this
* resource has been loaded.
* @param jarEntry The JAR/ZIP file entry describing this resource.
*/
private ArchiveClassPathResource(ArchiveClassPathEntry pathEntry,
JarEntry jarEntry) {
super(pathEntry, jarEntry.getName(), pathEntry.getProperty());
this.jarEntry = jarEntry;
}
/**
* Returns an URL to access this resource.
*
* @see ArchiveClassPathEntry#getURL(String)
*/
public URL getURL() {
return getArchiveClassPathEntry().getURL(getName());
}
/**
* Returns an URL identifying the archive from which this resource is
* loaded.
*
* @see ArchiveClassPathEntry#getCodeSourceURL()
*/
public URL getCodeSourceURL() {
return getArchiveClassPathEntry().getCodeSourceURL();
}
/**
* Returns an <code>InputStream</code> to read the contents of the
* resource. Calling this method actually accesses the JAR/ZIP file
* and seeks through the file until the entry is found.
* <p>
* Clients of this method must make sure to close the stream returned
* if not used anymore to prevent resource drain.
*
* @throws RepositoryException If an error occurrs accessing or reading
* the archive.
*
* @see ArchiveClassPathEntry#findEntry(JarInputStream, String)
*/
public InputStream getInputStream() throws RepositoryException {
/**
* Cannot reuse the ClassPathEntry instances entry and
* JarInputStream, because this is shared and has to be
* closed to release the property resource.
*/
JarInputStream zipIns = null;
JarEntry entry = null;
try {
zipIns = getJarInputStream(getProperty());
entry = getArchiveClassPathEntry().findEntry(zipIns, getName());
if (entry != null) {
return zipIns;
}
// otherwise
log.warn("Cannot find entry " + getName() + " in the archive "
+ getClassPathEntry().getPath() + " anymore!");
return null;
} catch (IOException ioe) {
// log
throw new RepositoryException(ioe);
} finally {
// if thrown during findEntry(), entry is null but
// the stream is open. As we exit by an exception,
// the InputStream is not returned and must be
// closed to release it.
if (entry == null && zipIns != null) {
try {
zipIns.close();
} catch (IOException ignored) {
}
}
}
}
/**
* Returns the value of the <code>size</code> field of the JAR/ZIP
* file entry of this resource. If the size is not known to the entry,
* <code>-1</code> may be returned.
*/
public int getContentLength() {
return (int) jarEntry.getSize();
}
/**
* Returns the path to the property containing the archive or the
* path with which the {@link ArchiveClassPathEntry} was created if the
* former cannot be retrieved.
*/
public String getPath() {
try {
return getProperty().getPath();
} catch (RepositoryException re) {
String archivePath = getClassPathEntry().getPath();
log.warn("Cannot access the path of the archive " +
"property below " + archivePath, re);
return archivePath;
}
}
/**
* Returns the value of the <code>time</code> field of the JAR/ZIP
* file entry of this resource. If the time is not known to the entry,
* <code>-1</code> may be returned.
*/
public long getLastModificationTime() {
return jarEntry.getTime();
}
/**
* Returns the manifest of the archive from which this resource was
* loaded or <code>null</code> if no such manifest exists or an error
* occurrs reading the manifest.
*
* @see ArchiveClassPathEntry#getManifest()
*/
public Manifest getManifest() {
return getArchiveClassPathEntry().getManifest();
}
/**
* Returns the {@link ArchiveClassPathEntry} from which this resource
* was loaded.
*/
protected ArchiveClassPathEntry getArchiveClassPathEntry() {
return (ArchiveClassPathEntry) getClassPathEntry();
}
}
}