blob: b9a954d48e31ef5e32552c771c14215e6b6d9fc7 [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.URL;
import java.net.URLConnection;
import java.util.Calendar;
import java.util.StringTokenizer;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.jcr.Item;
import javax.jcr.Node;
import javax.jcr.Property;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Workspace;
import javax.jcr.nodetype.NoSuchNodeTypeException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* The <code>ExpandingArchiveClassPathEntry</code> extends the
* {@link org.apache.jackrabbit.classloader.ArchiveClassPathEntry} class with support
* to automatically expand the archive (JAR or ZIP) into the repository
* below the path entry node. 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
*
* @see org.apache.jackrabbit.classloader.ArchiveClassPathEntry
* @see org.apache.jackrabbit.classloader.ClassPathEntry
*/
/* package */ class ExpandingArchiveClassPathEntry extends ArchiveClassPathEntry {
/** The name of the node type required to expand the archive */
public static final String TYPE_JARFILE = "rep:jarFile";
/** The name of the child node taking the expanded archive */
public static final String NODE_JARCONTENTS = "rep:jarContents";
/**
* The name of the property taking the time at which the archive was
* expanded
*/
public static final String PROP_EXPAND_DATE = "rep:jarExpanded";
/** Default logger */
private static final Log log =
LogFactory.getLog(ExpandingArchiveClassPathEntry.class);
/** The node of the unpacked JAR contents */
private Node jarContents;
/**
* Creates an instance of the <code>ExpandingArchiveClassPathEntry</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
* property's path if the property was found through the primary
* item chain.
*
* @throws RepositoryException If an error occurrs retrieving the session
* from the property.
*/
ExpandingArchiveClassPathEntry(Property prop, String path)
throws RepositoryException {
super(prop, path);
}
/**
* Clones the indicated <code>ExpandingArchiveClassPathEntry</code> object
* by taking over its path, session and property.
*
* @param base The base <code>ExpandingArchiveClassPathEntry</code> entry
* to clone.
*
* @see ClassPathEntry#ClassPathEntry(ClassPathEntry)
*/
private ExpandingArchiveClassPathEntry(ExpandingArchiveClassPathEntry base) {
super(base);
}
/**
* 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.
*
* @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) {
try {
// find the resource for the name in the expanded archive contents
Node jarContents = getJarContents();
Item resItem = null;
if (jarContents.hasNode(name)) {
resItem = jarContents.getNode(name);
} else if (jarContents.hasProperty(name)) {
resItem = jarContents.getProperty(name);
}
// if the name resolved to an item, resolve the item to a
// single-valued non-reference property
Property resProp = (resItem != null)
? Util.getProperty(resItem)
: null;
// if found create the resource to return
if (resProp != null) {
return new ClassLoaderResource(this, name, resProp) {
public URL getURL() {
return ExpandingArchiveClassPathEntry.this.getURL(getName());
}
public URL getCodeSourceURL() {
return ExpandingArchiveClassPathEntry.this.getCodeSourceURL();
}
public Manifest getManifest() {
return ExpandingArchiveClassPathEntry.this.getManifest();
}
protected Property getExpiryProperty() {
return ExpandingArchiveClassPathEntry.this.getProperty();
}
};
}
log.debug("getResource: resource " + name + " not found"
+ " in archive " + path);
} catch (RepositoryException re) {
log.warn("getResource: problem accessing the archive " + path
+ " for " + name + ": " + re.toString());
}
// 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>ExpandingArchiveClassPathEntry</code> class has internal state.
* Therefore a new instance is created from the unmodifiable configuration
* of this instance.
*/
ClassPathEntry copy() {
return new ExpandingArchiveClassPathEntry(this);
}
//----------- internal helper to find the entry ------------------------
/**
* Returns the root node of the expanded archive. If the archive's node
* does not contain the expanded archive, it is expanded on demand. If the
* archive has already been expanded, it is checked whether it is up to
* date and expanded again if not.
*
* @throws RepositoryException if an error occurrs expanding the archive
* into the repository.
*/
private Node getJarContents() throws RepositoryException {
if (jarContents == null) {
Node jarNode = null; // the node containing the jar file
Node jarRoot = null; // the root node of the expanded contents
try {
Item jarItem = session.getItem(getPath());
jarNode = (jarItem.isNode()) ? (Node) jarItem : jarItem.getParent();
// if the jar been unpacked once, check for updated jar file,
// which must be unpacked
if (jarNode.isNodeType(TYPE_JARFILE)) {
long lastMod = Util.getLastModificationTime(getProperty());
long expanded =
jarNode.getProperty(PROP_EXPAND_DATE).getLong();
// get the content, remove if outdated or use if ok
jarRoot = jarNode.getNode(NODE_JARCONTENTS);
// if expanded content is outdated, remove it
if (lastMod <= expanded) {
jarRoot.remove();
jarRoot = null; // have to unpack below
}
} else if (!jarNode.canAddMixin(TYPE_JARFILE)) {
// this is actually a problem, because I expect to be able
// to add the mixin node type due to checkExpandArchives
// having returned true earlier
throw new RepositoryException(
"Cannot unpack JAR file contents into "
+ jarNode.getPath());
} else {
jarNode.addMixin(TYPE_JARFILE);
jarNode.setProperty(PROP_EXPAND_DATE, Calendar.getInstance());
}
// if the content root is not set, unpack and save
if (jarRoot == null) {
jarRoot = jarNode.addNode(NODE_JARCONTENTS, "nt:folder");
unpack(jarRoot);
jarNode.save();
}
} finally {
// rollback changes on the jar node in case of problems
if (jarNode != null && jarNode.isModified()) {
// rollback incomplete modifications
log.warn("Rolling back unsaved changes on JAR node "
+ getPath());
try {
jarNode.refresh(false);
} catch (RepositoryException re) {
log.warn("Cannot rollback changes after failure to " +
"expand " + getPath(), re);
}
}
}
jarContents = jarRoot;
}
return jarContents;
}
/**
* Expands the archive stored in the property of this class path entry into
* the repositroy below the given <code>jarRoot</code> node.
* <p>
* This method leaves the subtree at and below <code>jarRoot</code> unsaved.
* It is the task of the caller to save or rollback as appropriate.
*
* @param jarRoot The <code>Node</code> below which the archive is to be
* unpacked.
*
* @throws RepositoryException If an error occurrs creating the item
* structure to unpack the archive or if an error occurrs reading
* the archive.
*/
private void unpack(Node jarRoot) throws RepositoryException {
ZipInputStream zin = null;
try {
zin = new ZipInputStream(getProperty().getStream());
ZipEntry entry = zin.getNextEntry();
while (entry != null) {
if (entry.isDirectory()) {
unpackFolder(jarRoot, entry.getName());
} else {
unpackFile(jarRoot, entry, zin);
}
entry = zin.getNextEntry();
}
} catch (IOException ioe) {
throw new RepositoryException(
"Problem reading JAR contents of " + getPath(), ioe);
} finally {
// close the JAR stream if open
if (zin != null) {
try {
zin.close();
} catch (IOException ignore) {}
}
}
}
/**
* Makes sure a node exists at the <code>path</code> relative to
* <code>root</code>. In other words, this method returns the node
* <code>root.getNode(path)</code>, creating child nodes as required. Newly
* created nodes are created with node type <code>nt:folder</code>.
* <p>
* If intermediate nodes or the actual node required already exist, they
* must be typed such, that they may either accept child node creations
* of type <code>nt:file</code> or <code>nt:folder</code>.
*
* @param root The <code>Node</code> relative to which a node representing
* a folder is to created if required.
* @param path The path relative to <code>root</code> of the folder to
* ensure.
*
* @return The <code>Node</code> representing the folder below
* <code>root</code>.
*
* @throws RepositoryException If an error occurrs accessing the repository
* or creating missing node(s).
*/
private Node unpackFolder(Node root, String path) throws RepositoryException {
// remove trailing slash
while (path.endsWith("/")) {
path = path.substring(0, path.length()-1);
}
// quick check if the folder already exists
if (root.hasNode(path)) {
return root.getNode(path);
}
// go down and create the path
StringTokenizer tokener = new StringTokenizer(path, "/");
while (tokener.hasMoreTokens()) {
String label = tokener.nextToken();
if (root.hasNode(label)) {
root = root.getNode(label);
} else {
root = root.addNode(label, "nt:folder");
}
}
// return the final node
return root;
}
/**
* Creates a <code>nt:file</code> node with the path
* <code>entry.getName()</code> relative to the <code>root</code> node. The
* contents of the <code>jcr:content/jcr:data</code> property of the file
* node is retrieved from <code>ins</code>.
* <p>
* The <code>jcr:content/jcr:lastModified</code> property is set to the
* value of the <code>time</code> field of the <code>entry</code>. The
* <code>jcr:content/jcr:mimeType</code> property is set to a best-effort
* guess of the content type of the entry. To guess the content type, the
* <code>java.net.URLConnection.guessContentType(String)</code> method
* is called. If this results in no content type, the default
* <code>application/octet-stream</code> is set.
*
* @param root The node relative to which the <code>nt:file</code> node
* is created.
* @param entry The <code>ZipEntry</code> providing information on the
* file to be created. Namely the <code>name</code> and
* <code>time</code> fields are used.
* @param ins The <code>InputStream</code> providing the data to be written
* to the <code>jcr:content/jcr:data</code> property.
*
* @throws RepositoryException If an error occurrs creating and filling
* the <code>nt:file</code> node.
*/
private void unpackFile(Node root, ZipEntry entry, InputStream ins) throws RepositoryException {
int slash = entry.getName().lastIndexOf('/');
String label = entry.getName().substring(slash+1);
Node parent = (slash <= 0)
? root
: unpackFolder(root, entry.getName().substring(0, slash));
// remove existing node (and all children by the way !!)
if (parent.hasNode(label)) {
parent.getNode(label).remove();
}
// prepare property values
Calendar lastModified = Calendar.getInstance();
lastModified.setTimeInMillis(entry.getTime());
String mimeType = URLConnection.guessContentTypeFromName(label);
if (mimeType == null) {
mimeType = "application/octet-stream";
}
// create entry nodes
Node ntFile = parent.addNode(label, "nt:file");
Node content = ntFile.addNode("jcr:content", "nt:resource");
content.setProperty("jcr:mimeType", mimeType);
content.setProperty("jcr:data", ins);
content.setProperty("jcr:lastModified", lastModified);
}
/**
* Checks whether it is possible to use this class for archive class path
* entries in the workspace (and repository) to which the <code>session</code>
* provides access.
* <p>
* This method works as follows. If the node type <code>rep:jarFile</code>
* is defined in the session's repository, <code>true</code> is immediately
* returned. If an error checking for the node type, <code>false</code> is
* immediately returned.
* <p>
* If the node type is not defined, the
* {@link NodeTypeSupport#registerNodeType(Workspace)} method is called
* to register the node type. Any errors occurring while calling or
* executing this method is logged an <code>false</code> is returned.
* Otherwise, if node type registration succeeded, <code>true</code> is
* returned.
* <p>
* This method is synchronized such that two paralell threads do not try
* to create the node, which might yield wrong negatives.
*
* @param session The <code>Session</code> providing access to the
* repository.
*
* @return <code>true</code> if this class can be used to handle archive
* class path entries. See above for a description of the test used.
*/
/* package */ synchronized static boolean canExpandArchives(Session session) {
// quick check for the node type, succeed if defined
try {
session.getWorkspace().getNodeTypeManager().getNodeType(TYPE_JARFILE);
log.debug("Required node type exists, can expand archives");
return true;
} catch (NoSuchNodeTypeException nst) {
log.debug("Required node types does not exist, try to define");
} catch (RepositoryException re) {
log.info("Cannot check for required node type, cannot expand " +
"archives", re);
return false;
}
try {
Workspace workspace = session.getWorkspace();
return NodeTypeSupport.registerNodeType(workspace);
} catch (Throwable t) {
// Prevent anything from hapening if node type registration fails
// due to missing libraries or other errors
log.info("Error registering node type", t);
}
// fallback to failure
return false;
}
}