| /* |
| * 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; |
| } |
| } |