You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jackrabbit.apache.org by fm...@apache.org on 2005/12/21 15:14:24 UTC

svn commit: r358296 [3/5] - in /incubator/jackrabbit/trunk/contrib/classloader: ./ src/ src/main/ src/main/java/ src/main/java/org/ src/main/java/org/apache/ src/main/java/org/apache/jackrabbit/ src/main/java/org/apache/jackrabbit/classloader/ src/main...

Added: incubator/jackrabbit/trunk/contrib/classloader/src/main/java/org/apache/jackrabbit/classloader/RepositoryClassLoader.java
URL: http://svn.apache.org/viewcvs/incubator/jackrabbit/trunk/contrib/classloader/src/main/java/org/apache/jackrabbit/classloader/RepositoryClassLoader.java?rev=358296&view=auto
==============================================================================
--- incubator/jackrabbit/trunk/contrib/classloader/src/main/java/org/apache/jackrabbit/classloader/RepositoryClassLoader.java (added)
+++ incubator/jackrabbit/trunk/contrib/classloader/src/main/java/org/apache/jackrabbit/classloader/RepositoryClassLoader.java Wed Dec 21 06:13:56 2005
@@ -0,0 +1,865 @@
+/*
+ * Copyright 2004-2005 The Apache Software Foundation or its licensors,
+ *                     as applicable.
+ *
+ * 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.apache.jackrabbit.classloader;
+
+import java.beans.Introspector;
+import java.io.IOException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.security.AccessController;
+import java.security.PrivilegedExceptionAction;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.jackrabbit.net.JCRURLConnection;
+import org.apache.jackrabbit.net.URLFactory;
+
+
+/**
+ * The <code>RepositoryClassLoader</code> class extends the
+ * <code>URLClassLoader</code> and provides the functionality to load classes
+ * and resources from JCR Repository.
+ * <p>
+ * This class loader supports loading classes from the Repository hierarchy,
+ * such as a <em>classes</em> 'folder', but also from Jar and Zip files stored
+ * in the Repository.
+ * <p>
+ * For enhanced performance, this class loader keeps a list of resources and
+ * classes which have already been loaded through this class loader. If later
+ * requests ask for already cached resources, these are returned without
+ * checking whether the underlying repository actually still exists.
+ * <p>
+ * This class is not intended to be extended by clients.
+ *
+ * @author Felix Meschberger
+ * @version $Rev:$, $Date$
+ */
+public class RepositoryClassLoader extends URLClassLoader {
+
+    /** default log category */
+    private static final Log log =
+        LogFactory.getLog(RepositoryClassLoader.class);
+
+    /** An empty list of url paths to call superclass constructor */
+    private static final URL[] NULL_PATH = {};
+
+    /**
+     * The special resource representing a resource which could not be
+     * found in the class path.
+     *
+     * @see #cache
+     * @see #findClassLoaderResource(String)
+     */
+    /* package */ static final ClassLoaderResource NOT_FOUND_RESOURCE =
+        new ClassLoaderResource(null, "[sentinel]", null) {
+            public boolean isExpired() {
+                return false;
+            }
+        };
+
+    /**
+     * The classpath which this classloader searches for class definitions.
+     * Each element of the vector should be either a directory, a .zip
+     * file, or a .jar file.
+     * <p>
+     * It may be empty when only system classes are controlled.
+     */
+    private ClassPathEntry[] repository;
+
+    /**
+     * The list of handles to use as a classpath. These is the unprocessed
+     * list of handles given to the constructor.
+     */
+    private PatternPath handles;
+
+    /**
+     * The <code>Session</code> grants access to the Repository to access the
+     * resources.
+     * <p>
+     * This field is not final such that it may be cleared when the class loader
+     * is destroyed.
+     */
+    private Session session;
+
+    /**
+     * Cache of resources found or not found in the class path. The map is
+     * indexed by resource name and contains mappings to instances of the
+     * {@link ClassLoaderResource} class. If a resource has been tried to be
+     * loaded, which could not be found, the resource is cached with the
+     * special mapping to {@link #NOT_FOUND_RESOURCE}.
+     *
+     * @see #NOT_FOUND_RESOURCE
+     * @see #findClassLoaderResource(String)
+     */
+    private Map cache;
+
+    /**
+     * Flag indicating whether the {@link #destroy()} method has already been
+     * called (<code>true</code>) or not (<code>false</code>)
+     */
+    private boolean destroyed;
+
+    /**
+     * Creates a <code>RepositoryClassLoader</code> from a list of item path
+     * strings containing globbing pattens for the paths defining the class
+     * path.
+     *
+     * @param session The <code>Session</code> to use to access the class items.
+     * @param classPath The list of path strings making up the (initial) class
+     *      path of this class loader. The strings may contain globbing
+     *      characters which will be resolved to build the actual class path.
+     * @param parent The parent <code>ClassLoader</code>, which may be
+     *            <code>null</code>.
+     *
+     * @throws NullPointerException if either the session or the handles list is
+     *             <code>null</code>.
+     */
+    public RepositoryClassLoader(Session session, String[] classPath,
+        ClassLoader parent) {
+        this(session, new DynamicPatternPath(session, classPath), parent);
+    }
+
+    /**
+     * Creates a <code>RepositoryClassLoader</code> from a
+     * {@link PatternPath} containing globbing pattens for the handles
+     * defining the class path.
+     *
+     * @param session The <code>Session</code> to use to access the class items.
+     * @param handles The {@link PatternPath} of handles.
+     * @param parent The parent <code>ClassLoader</code>, which may be
+     *            <code>null</code>.
+     *
+     * @throws NullPointerException if either the session or the handles list is
+     *             <code>null</code>.
+     */
+    /* package */ RepositoryClassLoader(Session session, PatternPath handles,
+            ClassLoader parent) {
+
+        // initialize the super class with an empty class path
+        super(NULL_PATH, parent);
+
+        // check session and handles
+        if (session == null) {
+            throw new NullPointerException("session");
+        }
+        if (handles == null) {
+            throw new NullPointerException("handles");
+        }
+
+        // set fields
+        this.session = session;
+        this.setHandles(handles);
+        this.cache = new HashMap();
+        this.destroyed = false;
+
+        // build the class repositories list
+        buildRepository();
+
+        log.debug("RepositoryClassLoader: " + this + " ready");
+    }
+
+    /**
+     * Returns <code>true</code> if this class loader has already been destroyed
+     * by calling {@link #destroy()}.
+     */
+    protected boolean isDestroyed() {
+        return destroyed;
+    }
+
+    /**
+     * Destroys this class loader. This process encompasses all steps needed
+     * to remove as much references to this class loader as possible.
+     * <p>
+     * <em>NOTE</em>: This method just clears all internal fields and especially
+     * the class path to render this class loader unusable.
+     * <p>
+     * This implementation does not throw any exceptions.
+     */
+    public void destroy() {
+        // we expect to be called only once, so we stop destroyal here
+        if (isDestroyed()) {
+            log.debug("Instance is already destroyed");
+            return;
+        }
+
+        // set destroyal guard
+        destroyed = true;
+
+        // clear caches and references
+        setRepository(null);
+        setHandles(null);
+        session = null;
+
+        // clear the cache of loaded resources and flush cached class
+        // introspections of the JavaBean framework
+        if (cache != null) {
+            for (Iterator ci=cache.values().iterator(); ci.hasNext(); ) {
+                ClassLoaderResource res = (ClassLoaderResource) ci.next();
+                if (res.getLoadedClass() != null) {
+                    Introspector.flushFromCaches(res.getLoadedClass());
+                    res.setLoadedClass(null);
+                }
+                ci.remove();
+            }
+        }
+    }
+
+    //---------- URLClassLoader overwrites -------------------------------------
+
+    /**
+     * Finds and loads the class with the specified name from the class path.
+     *
+     * @param name the name of the class
+     * @return the resulting class
+     *
+     * @throws ClassNotFoundException If the named class could not be found or
+     *      if this class loader has already been destroyed.
+     */
+    protected Class findClass(final String name) throws ClassNotFoundException {
+
+        if (isDestroyed()) {
+            throw new ClassNotFoundException(name + " (Classloader destroyed)");
+        }
+
+        log.debug("findClass: Try to find class " + name);
+
+        try {
+            return (Class) AccessController
+                .doPrivileged(new PrivilegedExceptionAction() {
+
+                    public Object run() throws ClassNotFoundException {
+                        return findClassPrivileged(name);
+                    }
+                });
+        } catch (java.security.PrivilegedActionException pae) {
+            throw (ClassNotFoundException) pae.getException();
+        }
+    }
+
+    /**
+     * Finds the resource with the specified name on the 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 or if the class loader has
+     *      already been destroyed.
+     */
+    public URL findResource(String name) {
+
+        if (isDestroyed()) {
+            log.warn("Destroyed class loader cannot find a resource");
+            return null;
+        }
+
+        log.debug("findResource: Try to find resource " + name);
+
+        ClassLoaderResource res = findClassLoaderResource(name);
+        if (res != null) {
+            log.debug("findResource: Getting resource from " + res + ", " +
+                "created " + new Date(res.getLastModificationTime()));
+            return res.getURL();
+        }
+
+        return null;
+    }
+
+    /**
+     * Returns an Enumeration of URLs representing all of the resources
+     * on the search path having the specified name.
+     *
+     * @param name the resource name
+     *
+     * @return an <code>Enumeration</code> of <code>URL</code>s. This is an
+     *      empty enumeration if no resources are found by this class loader
+     *      or if this class loader has already been destroyed.
+     */
+    public Enumeration findResources(String name) {
+
+        if (isDestroyed()) {
+            log.warn("Destroyed class loader cannot find resources");
+            return new Enumeration() {
+                public boolean hasMoreElements() {
+                    return false;
+                }
+                public Object nextElement() {
+                    throw new NoSuchElementException("No Entries");
+                }
+            };
+        }
+
+        log.debug("findResources: Try to find resources for " + name);
+
+        List list = new LinkedList();
+        for (int i=0; i < repository.length; i++) {
+            final ClassPathEntry cp = repository[i];
+            log.debug("findResources: Trying " + cp);
+
+            ClassLoaderResource res = cp.getResource(name);
+            if (res != null) {
+                log.debug("findResources: Adding resource from " + res + ", " +
+                    "created " + new Date(res.getLastModificationTime()));
+                URL url = res.getURL();
+                if (url != null) {
+                    list.add(url);
+                }
+            }
+
+        }
+
+        // return the enumeration on the list
+        return Collections.enumeration(list);
+    }
+
+    /**
+     * 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 {@link #addURL(URL)}
+     * and {@link #addHandle(String)} methods.
+     *
+     * @return the search path of URLs for loading classes and resources. The
+     *      list is empty, if this class loader has already been destroyed.
+     */
+    public URL[] getURLs() {
+        if (isDestroyed()) {
+            log.warn("Destroyed class loader has no URLs any more");
+            return new URL[0];
+        }
+
+        List urls = new ArrayList();
+        for (int i=0; i < repository.length; i++) {
+            URL url = repository[i].toURL();
+            if (url != null) {
+                urls.add(url);
+            }
+        }
+        return (URL[]) urls.toArray(new URL[urls.size()]);
+    }
+
+    /**
+     * Appends the specified URL to the list of URLs to search for
+     * classes and resources. Only Repository URLs with the protocol set to
+     * <code>JCR</code> are considered for addition. The system will find out
+     * whether the URL points to a directory or a jar archive.
+     * <p>
+     * URLs added using this method will be preserved through reconfiguration
+     * and reinstantiation.
+     * <p>
+     * If this class loader has already been destroyed this method has no
+     * effect.
+     *
+     * @param url the <code>JCR</code> URL to be added to the search path of
+     *      URLs.
+     */
+    protected void addURL(URL url) {
+        if (isDestroyed()) {
+            log.warn("Cannot add URL to destroyed class loader");
+
+        } else if (checkURL(url)) {
+            // Repository URL
+            log.debug("addURL: Adding URL " + url);
+            try {
+                JCRURLConnection conn = (JCRURLConnection) url.openConnection();
+                ClassPathEntry cp = ClassPathEntry.getInstance(
+                    conn.getSession(), conn.getPath());
+                addClassPathEntry(cp);
+            } catch (IOException ioe) {
+                log.warn("addURL: Cannot add URL " + url, ioe);
+            }
+
+        } else {
+            log.warn("addURL: " + url + " is not a Repository URL, ignored");
+        }
+    }
+
+    /**
+     * Appends the specified path to the list of handles to search for classes
+     * and resources. The system will find out whether the path points to a
+     * directory or a JAR or ZIP archive. The path is added as is, provided it
+     * is valid to be used in the class path and therefore must not contain any
+     * globbing characters.
+     * <p>
+     * If this class loader has already been destroyed, this method has no
+     * effect.
+     *
+     * @param path The path to be added to the search path.
+     */
+    public void addHandle(String path) {
+        if (isDestroyed()) {
+            log.warn("Cannot add handle to destroyed class loader");
+            return;
+        }
+
+        log.debug("addURL: Adding Handle " + path);
+        ClassPathEntry cp = ClassPathEntry.getInstance(session, path);
+        if (cp != null) {
+            addClassPathEntry(cp);
+        } else {
+            log.debug("addHandle: Cannot get a ClassPathEntry for " + path);
+        }
+    }
+
+    //---------- Property access ----------------------------------------------
+
+    /**
+     * Sets the {@link PatternPath} list to be used as the initial search
+     * path of this class loader. This new list replaces the path pattern list
+     * set in the constructor or in a previous call to this method.
+     * <p>
+     * After setting the list, this class loader's class path has to be rebuilt
+     * by calling the {@link #buildRepository()} method.
+     *
+     * @param handles The {@link PatternPath} to set on this class loader.
+     */
+    /* package */ void setHandles(PatternPath handles) {
+        this.handles = handles;
+    }
+
+    /**
+     * Returns the current {@link PatternPath} from which the search path
+     * of this class loader is configured.
+     */
+    /* package */ PatternPath getHandles() {
+        return handles;
+    }
+
+    /**
+     * Returns the named {@link ClassLoaderResource} if it is contained in the
+     * cache. If the resource does not exist in the cache or has not been found
+     * in the class path at an earlier point in time, <code>null</code> is
+     * returned.
+     *
+     * @param name The name of the resource to retrieve from the cache.
+     *
+     * @return The named <code>ClassLoaderResource</code> or <code>null</code>
+     *      if not loaded.
+     *
+     * @throws NullPointerException If this class loader has already been
+     *      destroyed.
+     */
+    /* package */ ClassLoaderResource getCachedResource(String name) {
+        Object res = cache.get(name);
+        if (res == null || res == NOT_FOUND_RESOURCE) {
+            log.debug("Resource " + name + " no cached");
+            return null;
+        }
+
+        return (ClassLoaderResource) res;
+    }
+
+    /**
+     * Returns an <code>Iterator</code> on all resources in the cache. This
+     * iterator may also contain {@link #NOT_FOUND_RESOURCE sentinel} entries
+     * for resources, which failed to load. Callers of this method should take
+     * care to filter out such resources before acting on it.
+     *
+     * @throws NullPointerException If this class loader has already been
+     *      destroyed.
+     */
+    /* package */ Iterator getCachedResources() {
+        return cache.values().iterator();
+    }
+
+    /**
+     * Removes all entries from the cache of loaded resources, which mark
+     * resources, which have not been found as of yet.
+     *
+     * @throws NullPointerException If this class loader has already been
+     *      destroyed.
+     */
+    protected void cleanCache() {
+        for (Iterator ci=cache.values().iterator(); ci.hasNext(); ) {
+            if (ci.next() == NOT_FOUND_RESOURCE) {
+                ci.remove();
+            }
+        }
+    }
+
+    /**
+     * Returns <code>true</code>, if the cache is not empty. If the
+     * {@link #cleanCache()} method is not called before calling this method, a
+     * false positive result may be returned.
+     *
+     * @throws NullPointerException If this class loader has already been
+     *      destroyed.
+     */
+    protected boolean hasLoadedResources() {
+        return cache.isEmpty();
+    }
+
+    /**
+     * Returns the session used by this class loader to access the repository.
+     * If this class loader has already been destroyed, this <code>null</code>
+     * is returned.
+     */
+    protected Session getSession() {
+        return session;
+    }
+
+    /**
+     * Sets the current active class path to the list of class path entries.
+     */
+    protected void setRepository(ClassPathEntry[] classPath) {
+        this.repository = classPath;
+    }
+
+    /**
+     * Returns the current active class path entries list or <code>null</code>
+     * if this class loader has already been destroyed.
+     */
+    protected ClassPathEntry[] getRepository() {
+        return repository;
+    }
+
+    /**
+     * Adds the class path entry to the current class path list. If the class
+     * loader has already been destroyed, this method creates a single entry
+     * class path list with the new class path entry.
+     */
+    protected void addClassPathEntry(ClassPathEntry cpe) {
+        log.debug("addHandle: Adding path " + cpe.getPath());
+
+        // append the entry to the current class path
+        ClassPathEntry[] oldClassPath = getRepository();
+        ClassPathEntry[] newClassPath = addClassPathEntry(oldClassPath, cpe);
+        setRepository(newClassPath);
+    }
+
+    /**
+     * Helper method for class path handling to a new entry to an existing
+     * list and return the new list.
+     * <p>
+     * If <code>list</code> is <code>null</code> a new array is returned with
+     * a single element <code>newEntry</code>. Otherwise the array returned
+     * contains all elements of <code>list</code> and <code>newEntry</code>
+     * at the last position.
+     *
+     * @param list The array of class path entries, to which a new entry is
+     *      to be appended. This may be <code>null</code>.
+     * @param newEntry The new entry to append to the class path list.
+     *
+     * @return The extended class path list.
+     */
+    protected ClassPathEntry[] addClassPathEntry(ClassPathEntry[] list,
+            ClassPathEntry newEntry) {
+
+        // quickly define single entry array for the first entry
+        if (list == null) {
+            return new ClassPathEntry[]{ newEntry };
+        }
+
+        // create new array and copy old and new contents
+        ClassPathEntry[] newList = new ClassPathEntry[list.length+1];
+        System.arraycopy(list, 0, newList, 0, list.length);
+        newList[list.length] = newEntry;
+        return newList;
+    }
+
+    //---------- Object overwrite ---------------------------------------------
+
+    /**
+     * Returns a string representation of this instance.
+     */
+    public String toString() {
+        StringBuffer buf = new StringBuffer(getClass().getName());
+
+        if (isDestroyed()) {
+            buf.append(" - destroyed");
+        } else {
+            buf.append(": parent: { ");
+            buf.append(getParent());
+            buf.append(" }, user: ");
+            buf.append(session.getUserID());
+        }
+
+        return buf.toString();
+    }
+
+    //---------- internal ------------------------------------------------------
+
+    /**
+     * Builds the repository list from the list of path patterns and appends
+     * the path entries from any added handles. This method may be used multiple
+     * times, each time replacing the currently defined repository list.
+     *
+     * @throws NullPointerException If this class loader has already been
+     *      destroyed.
+     */
+    protected synchronized void buildRepository() {
+
+        // build new repository
+        List handles;
+        try {
+            handles = getHandles().getExpandedPaths();
+        } catch (RepositoryException re) {
+            log.error("Cannot expand handle list", re);
+            return;
+        }
+
+        List newRepository = new ArrayList(handles.size());
+
+        // build repository from path patterns
+        for (int i=0; i < handles.size(); i++) {
+            String entry = (String) handles.get(i);
+            ClassPathEntry cp = null;
+
+            // try to find repository based on this path
+            if (getRepository() != null) {
+                for (int j=0; j < repository.length; j++) {
+                    ClassPathEntry tmp = repository[i];
+                    if (tmp.getPath().equals(entry)) {
+                        cp = tmp;
+                        break;
+                    }
+                }
+            }
+
+            // not found, creating new one
+            if (cp == null) {
+                cp = ClassPathEntry.getInstance(session, entry);
+            }
+
+            if (cp != null) {
+                log.debug("Adding path " + entry);
+                newRepository.add(cp);
+            } else {
+                log.debug("Cannot get a ClassPathEntry for " + entry);
+            }
+        }
+
+        // replace old repository with new one
+        ClassPathEntry[] newClassPath = new ClassPathEntry[newRepository.size()];
+        newRepository.toArray(newClassPath);
+        setRepository(newClassPath);
+
+        // clear un-found resource cache
+        cleanCache();
+    }
+
+    /**
+     * Tries to find the class in the class path from within a
+     * <code>PrivilegedAction</code>. Throws <code>ClassNotFoundException</code>
+     * if no class can be found for the name.
+     *
+     * @param name the name of the class
+     *
+     * @return the resulting class
+     *
+     * @throws ClassNotFoundException if the class could not be found
+     * @throws NullPointerException If this class loader has already been
+     *      destroyed.
+     */
+    private Class findClassPrivileged(String name) throws ClassNotFoundException {
+
+        // prepare the name of the class
+        final String path = name.replace('.', '/').concat(".class");
+        log.debug("findClassPrivileged: Try to find path " + path +
+            " for class " + name);
+
+        ClassLoaderResource res = findClassLoaderResource(path);
+        if (res != null) {
+
+             // try defining the class, error aborts
+             try {
+                 log.debug("findClassPrivileged: Loading class from " +
+                     res + ", created " + new Date(res.getLastModificationTime()));
+
+                 Class c = defineClass(name, res);
+                 if (c == null) {
+                     log.warn("defineClass returned null for class " + name);
+                     throw new ClassNotFoundException(name);
+                 }
+                 return c;
+
+             } catch (IOException ioe) {
+                 log.debug("defineClass failed", ioe);
+                 throw new ClassNotFoundException(name, ioe);
+             } catch (Throwable t) {
+                 log.debug("defineClass failed", t);
+                 throw new ClassNotFoundException(name, t);
+             }
+         }
+
+        throw new ClassNotFoundException(name);
+     }
+
+    /**
+     * Returns a {@link ClassLoaderResource} for the given <code>name</code> or
+     * <code>null</code> if not existing. If the resource has already been
+     * loaded earlier, the cached instance is returned. If the resource has
+     * not been found in an earlier call to this method, <code>null</code> is
+     * returned. Otherwise the resource is looked up in the class path. If
+     * found, the resource is cached and returned. If not found, the
+     * {@link #NOT_FOUND_RESOURCE} is cached for the name and <code>null</code>
+     * is returned.
+     *
+     * @param name The name of the resource to return.
+     *
+     * @return The named <code>ClassLoaderResource</code> if found or
+     *      <code>null</code> if not found.
+     *
+     * @throws NullPointerException If this class loader has already been
+     *      destroyed.
+     */
+    /* package */ ClassLoaderResource findClassLoaderResource(String name) {
+
+        // check for cached resources first
+        ClassLoaderResource res = (ClassLoaderResource) cache.get(name);
+        if (res == NOT_FOUND_RESOURCE) {
+            log.info("Resource '" + name + "' known to not exist in class path");
+            return null;
+        } else if (res != null) {
+            return res;
+        }
+
+        // walk the repository list and try to find the resource
+        for (int i = 0; i < repository.length; i++) {
+            final ClassPathEntry cp = repository[i];
+            log.debug("Checking " + cp);
+
+            res = cp.getResource(name);
+            if (res != null) {
+                log.debug("Found resource in " + res + ", " +
+                    "created " + new Date(res.getLastModificationTime()));
+                cache.put(name, res);
+                return res;
+            }
+
+        }
+
+        log.debug("No classpath entry contains " + name);
+        cache.put(name, NOT_FOUND_RESOURCE);
+        return null;
+    }
+
+    /**
+     * Defines a class getting the bytes for the class from the resource
+     *
+     * @param name The fully qualified class name
+     * @param res The resource to obtain the class bytes from
+     *
+     * @throws RepositoryException If a problem occurrs getting at the data.
+     * @throws IOException If a problem occurrs reading the class bytes from
+     *      the resource.
+     * @throws ClassFormatError If the class bytes read from the resource are
+     *      not a valid class.
+     */
+    private Class defineClass(String name, ClassLoaderResource res)
+            throws IOException, RepositoryException {
+
+        log.debug("defineClass(" + name + ", " + res + ")");
+
+        Class clazz = res.getLoadedClass();
+        if (clazz == null) {
+
+            /**
+             * This following code for packages is duplicate from URLClassLoader
+             * because it is private there. I would like to not be forced to
+             * do this, but I still have to find a way ... -fmeschbe
+             */
+
+            // package support
+            int i = name.lastIndexOf('.');
+            if (i != -1) {
+                String pkgname = name.substring(0, i);
+                // Check if package already loaded.
+                Package pkg = getPackage(pkgname);
+                URL url = res.getCodeSourceURL();
+                Manifest man = res.getManifest();
+                if (pkg != null) {
+                    // Package found, so check package sealing.
+                    boolean ok;
+                    if (pkg.isSealed()) {
+                        // Verify that code source URL is the same.
+                        ok = pkg.isSealed(url);
+                    } else {
+                        // Make sure we are not attempting to seal the package
+                        // at this code source URL.
+                        ok = (man == null) || !isSealed(pkgname, man);
+                    }
+                    if (!ok) {
+                        throw new SecurityException("sealing violation");
+                    }
+                } else {
+                    if (man != null) {
+                        definePackage(pkgname, man, url);
+                    } else {
+                        definePackage(pkgname, null, null, null, null, null, null, null);
+                    }
+                }
+            }
+
+            byte[] data = res.getBytes();
+            clazz = defineClass(name, data, 0, data.length);
+            res.setLoadedClass(clazz);
+        }
+
+        return clazz;
+    }
+
+    /**
+     * Returns true if the specified package name is sealed according to the
+     * given manifest
+     * <p>
+     * This code is duplicate from <code>URLClassLoader.isSealed</code> because
+     * the latter has private access and we need the method here.
+     */
+    private boolean isSealed(String name, Manifest man) {
+         String path = name.replace('.', '/').concat("/");
+         Attributes attr = man.getAttributes(path);
+         String sealed = null;
+         if (attr != null) {
+             sealed = attr.getValue(Attributes.Name.SEALED);
+         }
+         if (sealed == null) {
+             if ((attr = man.getMainAttributes()) != null) {
+                 sealed = attr.getValue(Attributes.Name.SEALED);
+             }
+         }
+         return "true".equalsIgnoreCase(sealed);
+    }
+
+    /**
+     * Returns <code>true</code> if the <code>url</code> is a <code>JCR</code>
+     * URL.
+     *
+     * @param url The URL to check whether it is a valid <code>JCR</code> URL.
+     *
+     * @return <code>true</code> if <code>url</code> is a valid <code>JCR</code>
+     *      URL.
+     *
+     * @throws NullPointerException if <code>url</code> is <code>null</code>.
+     */
+    private boolean checkURL(URL url) {
+        return URLFactory.REPOSITORY_SCHEME.equalsIgnoreCase(url.getProtocol());
+    }
+}
\ No newline at end of file

Propchange: incubator/jackrabbit/trunk/contrib/classloader/src/main/java/org/apache/jackrabbit/classloader/RepositoryClassLoader.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: incubator/jackrabbit/trunk/contrib/classloader/src/main/java/org/apache/jackrabbit/classloader/RepositoryClassLoader.java
------------------------------------------------------------------------------
    svn:keywords = author date id revision url

Added: incubator/jackrabbit/trunk/contrib/classloader/src/main/java/org/apache/jackrabbit/classloader/Util.java
URL: http://svn.apache.org/viewcvs/incubator/jackrabbit/trunk/contrib/classloader/src/main/java/org/apache/jackrabbit/classloader/Util.java?rev=358296&view=auto
==============================================================================
--- incubator/jackrabbit/trunk/contrib/classloader/src/main/java/org/apache/jackrabbit/classloader/Util.java (added)
+++ incubator/jackrabbit/trunk/contrib/classloader/src/main/java/org/apache/jackrabbit/classloader/Util.java Wed Dec 21 06:13:56 2005
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2004-2005 The Apache Software Foundation or its licensors,
+ *                     as applicable.
+ *
+ * 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.apache.jackrabbit.classloader;
+
+import javax.jcr.AccessDeniedException;
+import javax.jcr.Item;
+import javax.jcr.ItemNotFoundException;
+import javax.jcr.Node;
+import javax.jcr.PathNotFoundException;
+import javax.jcr.Property;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.ValueFormatException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * The <code>Util</code> provides helper methods for the repository classloader
+ * and its class path entry and resource classes.
+ * <p>
+ * This class may not be extended or instantiated, it just contains static
+ * utility methods.
+ *
+ * @author Felix Meschberger
+ * @version $Rev:$, $Date$
+ */
+public class Util {
+
+    /** default logging */
+    private static final Log log = LogFactory.getLog(Util.class);
+
+    /** Private constructor to not instantiate */
+    private Util() {}
+
+    /**
+     * Resolves the given <code>item</code> to a <code>Property</code>. If the
+     * <code>item</code> is a node, the <code>getPrimaryItem</code> method is
+     * called repeatedly until a property is returned or until no more primary
+     * item is available. If the resulting property is a multivalue property,
+     * <code>null</code> is returned. Otherwise if the resulting property is
+     * a <code>REFERENCE</code> property, the node referred to is retrieved
+     * and this method is called recursively with the node. Otherwise, the
+     * resulting property is returned.
+     *
+     * @param item The <code>Item</code> to resolve to a <code>Property</code>.
+     *
+     * @return The resolved <code>Property</code> or <code>null</code> if the
+     *      resolved property is a multi-valued property.
+     *
+     * @throws ItemNotFoundException If the <code>item</code> is a node which
+     *      cannot be resolved to a property through (repeated) calls to
+     *      <code>Node.getPrimaryItem</code>.
+     * @throws ValueFormatException If the <code>item</code> resolves to a
+     *      single-valued <code>REFERENCE</code> type property which cannot
+     *      be resolved to the node referred to.
+     * @throws RepositoryException if another error occurrs accessing the
+     *      repository.
+     */
+    public static Property getProperty(Item item)
+            throws ItemNotFoundException, ValueFormatException,
+            RepositoryException {
+
+        // if the item is a node, get its primary item until either
+        // no primary item exists any more or an ItemNotFoundException is thrown
+        while (item.isNode()) {
+            item = ((Node) item).getPrimaryItem();
+        }
+
+        // we get here with a property - otherwise an exception has already
+        // been thrown
+        Property prop = (Property) item;
+        if (prop.getDefinition().isMultiple()) {
+            log.error(prop.getPath() + " is a multivalue property");
+            return null;
+        } else if (prop.getType() == PropertyType.REFERENCE) {
+            Node node = prop.getNode();
+            log.info("Property " + prop.getPath() + " refers to node " +
+                node.getPath() + "; finding primary item");
+            return getProperty(node);
+        }
+
+        return prop;
+    }
+
+    /**
+     * Returns the last modification time of the property. If the property's
+     * parent node is a <code>nt:resource</code> the <code>long</code> value
+     * of the <code>jcr:lastModified</code> property of the parent node is
+     * returned. Otherwise the current system time is returned.
+     *
+     * @param prop The property for which to return the last modification
+     *      time.
+     *
+     * @return The last modification time of the resource or the current time
+     *      if the property is not a child of an <code>nt:resource</code> node.
+     *
+     * @throws ItemNotFoundException If the parent node of the property cannot
+     *      be retrieved.
+     * @throws PathNotFoundException If the "jcr:lastModified" property of the
+     *      parent node cannot be retrieved. This exception is unlikely in a
+     *      correctly configured repository as the jcr:lastModified property
+     *      has to be present in a node of type nt:resource.
+     * @throws AccessDeniedException If (read) access to the parent node is
+     *      denied.
+     * @throws RepositoryException If any other error occurrs accessing the
+     *      repository to retrieve the last modification time.
+     */
+    public static long getLastModificationTime(Property prop)
+            throws ItemNotFoundException, PathNotFoundException,
+            AccessDeniedException, RepositoryException {
+
+        Node parent = prop.getParent();
+        if (parent.isNodeType("nt:resource")) {
+            return parent.getProperty("jcr:lastModified").getLong();
+        }
+
+        return System.currentTimeMillis();
+    }
+}

Propchange: incubator/jackrabbit/trunk/contrib/classloader/src/main/java/org/apache/jackrabbit/classloader/Util.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: incubator/jackrabbit/trunk/contrib/classloader/src/main/java/org/apache/jackrabbit/classloader/Util.java
------------------------------------------------------------------------------
    svn:keywords = author date id revision url

Added: incubator/jackrabbit/trunk/contrib/classloader/src/main/java/org/apache/jackrabbit/net/FileParts.java
URL: http://svn.apache.org/viewcvs/incubator/jackrabbit/trunk/contrib/classloader/src/main/java/org/apache/jackrabbit/net/FileParts.java?rev=358296&view=auto
==============================================================================
--- incubator/jackrabbit/trunk/contrib/classloader/src/main/java/org/apache/jackrabbit/net/FileParts.java (added)
+++ incubator/jackrabbit/trunk/contrib/classloader/src/main/java/org/apache/jackrabbit/net/FileParts.java Wed Dec 21 06:13:56 2005
@@ -0,0 +1,313 @@
+/*
+ * Copyright 2004-2005 The Apache Software Foundation or its licensors,
+ *                     as applicable.
+ *
+ * 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.apache.jackrabbit.net;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+
+import javax.jcr.Session;
+
+/**
+ * The <code>FileParts</code> class provides composing and parsing functionality
+ * to create and analize JCR Repository URL file components.
+ * <p>
+ * The file component of a JCR Repository URL has the format
+ * <pre>
+ *      file = [ "jcr:" [ "//" authority ] ] "/" repository "/" workspace jarpath .
+ *      authority = // URL authority specification
+ *      repository = // URL encoded repository name
+ *      workspace = // URL encoded workspace name
+ *      jarpath = path [ "!/" [ entry ] ] .
+ *      path = // The absolute item path (with a leading slash)
+ *      entry = // The (relative) path to the entry in an archive
+ * </pre>
+ * <p>
+ * To facitility use of this class with JCRJar URLs, the
+ * {@link #FileParts(String)} supports file specifications which contains
+ * the JCR Repository URL scheme name and an optional URL authority
+ * specification. This prefix in front of the real file specification is
+ * silently discarded. It is not included in the string representation returned
+ * by the {@link #toString()} method.
+ * <p>
+ * To make sure parsing is not complicated by implementation and use case
+ * specific repository and workspace names, those names are URL encoded using
+ * the <code>URLEncoder</code> class and <i>UTF-8</i> character encoding.
+ *
+ * @author Felix Meschberger
+ * @version $Rev:$, $Date:$
+ */
+class FileParts {
+
+    /** The decoded name of the repository */
+    private final String repository;
+
+    /** The decoded name of the workspace */
+    private final String workspace;
+
+    /** The repository item path part of the URL path */
+    private final String path;
+
+    /**
+     * The path to the entry in the archive, if the file spec contains the
+     * jar entry separator <i>!/</i>. If no entry path is specified, this is
+     * <code>null</code>. If no path is specified after the <i>!/</i> this
+     * is an empty string.
+     */
+    private final String entryPath;
+
+    /**
+     * Creates a new instance for the root node of the given session. The
+     * repository name is currently set to the fixed string "_" as there has not
+     * been established a repository naming convention yet. The name of the
+     * workspace is set to the name of the workspace to which the session is
+     * attached. The path is set to <code>"/"</code> to indicate the root node
+     * if the <code>path</code> argument is <code>null</code>.
+     *
+     * @param session The session for which to create this instance.
+     * @param path The absolute item path to initialize this instance with. If
+     *      <code>null</code> the item path is set to the <code>/</code>.
+     * @param entryPath The path to the archive entry to set on this instance.
+     *      This is expected to be a relative path without a leading slash and
+     *      may be <code>null</code>.
+     *
+     * @throws NullPointerException if <code>session</code> is
+     *      <code>null</code>.
+     */
+    FileParts(Session session, String path, String entryPath) {
+        this.repository = "_";
+        this.workspace = session.getWorkspace().getName();
+        this.path = (path == null) ? "/" : path;
+        this.entryPath = entryPath;
+    }
+
+    /**
+     * Creates an instance of this class setting the repository, workspace and
+     * path fields from the given <code>file</code> specification.
+     *
+     * @param file The specification providing the repository, workspace and
+     *      path values.
+     *
+     * @throws NullPointerException if <code>file</code> is
+     *      <code>null</code>.
+     * @throws IllegalArgumentException if <code>file</code> is not the
+     *      correct format.
+     */
+    FileParts(String file) {
+        if (!file.startsWith("/")) {
+            if (file.startsWith(URLFactory.REPOSITORY_SCHEME+":")) {
+                file = strip(file);
+            } else {
+                throw failure("Not an absolute file", file);
+            }
+        }
+
+        // find the repository name
+        int slash0 = 1;
+        int slash1 = file.indexOf('/', slash0);
+        if (slash1 < 0 || slash1-slash0 == 0) {
+            throw failure("Missing repository name", file);
+        }
+        this.repository = decode(file.substring(slash0, slash1));
+
+        // find the workspace name
+        slash0 = slash1 + 1;
+        slash1 = file.indexOf('/', slash0);
+        if (slash1 < 0 || slash1-slash0 == 0) {
+            throw failure("Missing workspace name", file);
+        }
+        this.workspace = decode(file.substring(slash0, slash1));
+
+        String fullPath = file.substring(slash1);
+        int bangSlash = JCRJarURLHandler.indexOfBangSlash(fullPath);
+        if (bangSlash < 0) {
+            this.path = fullPath;
+            this.entryPath = null;
+        } else {
+            this.path = fullPath.substring(0, bangSlash-1);
+            this.entryPath = fullPath.substring(bangSlash+1);
+        }
+    }
+
+    /**
+     * Returns the plain name of the repository.
+     */
+    String getRepository() {
+        return repository;
+    }
+
+    /**
+     * Returns the plain name of the workspace.
+     */
+    String getWorkspace() {
+        return workspace;
+    }
+
+    /**
+     * Returns the absolute repository path of the item.
+     */
+    String getPath() {
+        return path;
+    }
+
+    /**
+     * Returns the entry path of <code>null</code> if no entry exists.
+     */
+    String getEntryPath() {
+        return entryPath;
+    }
+
+    //---------- Object overwrites --------------------------------------------
+
+    /**
+     * Returns a hash code for this instance composed of the hash codes of the
+     * repository, workspace and path names.
+     */
+    public int hashCode() {
+        return getRepository().hashCode() +
+            17 * getWorkspace().hashCode() +
+            33 * getPath().hashCode();
+    }
+
+    /**
+     * Returns <code>true</code> if <code>obj</code> is the same as this or
+     * if other is a <code>FileParts</code> with the same path, workspace and
+     * repository. Otherwise <code>false</code> is returned.
+     */
+    public boolean equals(Object obj) {
+        if (obj == this) {
+            return true;
+        } else if (obj instanceof FileParts) {
+            FileParts other = (FileParts) obj;
+
+            // catch null entry path, fail if other has a defined entry path
+            if (getEntryPath() == null) {
+                if (other.getEntryPath() != null) {
+                    return false;
+                }
+            }
+
+            return getPath().equals(other.getPath()) &&
+                getWorkspace().equals(other.getWorkspace()) &&
+                getRepository().equals(other.getRepository()) &&
+                getEntryPath().equals(other.getEntryPath());
+        }
+
+        // fall back on null or other class
+        return false;
+    }
+
+    /**
+     * Returns the encoded string representation of this instance, which may
+     * later be fed to the {@link #FileParts(String)} constructor to recreate
+     * an equivalent instance.
+     */
+    public String toString() {
+        StringBuffer buf = new StringBuffer();
+        buf.append('/').append(encode(getRepository()));
+        buf.append('/').append(encode(getWorkspace()));
+        buf.append(getPath());
+
+        if (getEntryPath() != null) {
+            buf.append("!/").append(getEntryPath());
+        }
+
+        return buf.toString();
+    }
+
+    //---------- internal -----------------------------------------------------
+
+    /**
+     * @throws IllegalArgumentException If there is no path element after the
+     *      authority.
+     */
+    private String strip(String file) {
+        // cut off jcr: prefix - any other prefix, incl. double slash
+        // would cause an exception to be thrown in the constructor
+        int start = 4;
+
+        // check whether the remainder contains an authority specification
+        if (file.length() >= start+2 && file.charAt(start) == '/' &&
+                file.charAt(start+1) == '/') {
+
+            // find the slash after the authority, fail if missing
+            start = file.indexOf('/', start + 2);
+            if (start < 0) {
+                throw failure("Missing path after authority", file);
+            }
+        }
+
+        // return the file now
+        return file.substring(start);
+    }
+
+    /**
+     * Encodes the given string value using the <code>URLEncoder</code> and
+     * <i>UTF-8</i> character encoding.
+     *
+     * @param value The string value to encode.
+     *
+     * @return The encoded string value.
+     *
+     * @throws InternalError If <code>UTF-8</code> character set encoding is
+     *      not supported. As <code>UTF-8</code> is required to be implemented
+     *      on any Java platform, this error is not expected.
+     */
+    private String encode(String value) {
+        try {
+            return URLEncoder.encode(value, "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            // not expected, throw an InternalError
+            throw new InternalError("UTF-8 not supported");
+        }
+    }
+
+    /**
+     * Decodes the given string value using the <code>URLDecoder</code> and
+     * <i>UTF-8</i> character encoding.
+     *
+     * @param value The string value to decode.
+     *
+     * @return The decoded string value.
+     *
+     * @throws InternalError If <code>UTF-8</code> character set encoding is
+     *      not supported. As <code>UTF-8</code> is required to be implemented
+     *      on any Java platform, this error is not expected.
+     */
+    private String decode(String value) {
+        try {
+            return URLDecoder.decode(value, "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            // not expected, throw an InternalError
+            throw new InternalError("UTF-8 not supported");
+        }
+    }
+
+    /**
+     * Returns a <code>IllegalArgumentException</code> formatted with the
+     * given reason and causing file specification.
+     *
+     * @param reason The failure reason.
+     * @param file The original file specification leading to failure.
+     *
+     * @return A <code>IllegalArgumentException</code> with the given
+     *      reason and causing file specification.
+     */
+    private IllegalArgumentException failure(String reason, String file) {
+        return new IllegalArgumentException(reason + ": '" + file + "'");
+    }
+}
\ No newline at end of file

Propchange: incubator/jackrabbit/trunk/contrib/classloader/src/main/java/org/apache/jackrabbit/net/FileParts.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: incubator/jackrabbit/trunk/contrib/classloader/src/main/java/org/apache/jackrabbit/net/FileParts.java
------------------------------------------------------------------------------
    svn:keywords = author date id revision url

Added: incubator/jackrabbit/trunk/contrib/classloader/src/main/java/org/apache/jackrabbit/net/JCRJarURLConnection.java
URL: http://svn.apache.org/viewcvs/incubator/jackrabbit/trunk/contrib/classloader/src/main/java/org/apache/jackrabbit/net/JCRJarURLConnection.java?rev=358296&view=auto
==============================================================================
--- incubator/jackrabbit/trunk/contrib/classloader/src/main/java/org/apache/jackrabbit/net/JCRJarURLConnection.java (added)
+++ incubator/jackrabbit/trunk/contrib/classloader/src/main/java/org/apache/jackrabbit/net/JCRJarURLConnection.java Wed Dec 21 06:13:56 2005
@@ -0,0 +1,289 @@
+/*
+ * Copyright 2004-2005 The Apache Software Foundation or its licensors,
+ *                     as applicable.
+ *
+ * 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.apache.jackrabbit.net;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.jar.JarEntry;
+import java.util.jar.JarInputStream;
+
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * The <code>JCRJarURLConnection</code> extends the
+ * {@link org.apache.jackrabbit.net.JCRURLConnection} class to support accessing
+ * archive files stored in a JCR Repository.
+ * <p>
+ * Just like the base class, this class requires the URL to resolve, either
+ * directly or through primary item chain, to a repository <code>Property</code>.
+ * <p>
+ * Access to this connections property and archive entry content is perpared
+ * with the {@link #connect()}, which after calling the base class implementation
+ * to find the property tries to find the archive entry and set the connection's
+ * fields according to the entry. This implementation's {@link #connect()}
+ * method fails if the named entry does not exist in the archive.
+ * <p>
+ * The {@link #getInputStream()} method either returns an stream on the archive
+ * entry or on the archive depending on whether an entry path is specified
+ * in the URL or not. Like the base class implementation, this implementation
+ * returns a new <code>InputStream</code> on each invocation.
+ * <p>
+ * If an entry path is defined on the URL, the header fields are set from the
+ * archive entry:
+ * <table border="0" cellspacing="0" cellpadding="3">
+ *  <tr><td><code>Content-Type</code><td>Guessed from the entry name or
+ *      <code>application/octet-stream</code> if the type cannot be guessed
+ *      from the name</tr>
+ *  <tr><td><code>Content-Encoding</code><td><code>null</code></tr>
+ *  <tr><td><code>Content-Length</code><td>The size of the entry</tr>
+ *  <tr><td><code>Last-Modified</code><td>The last modification time of the
+ *      entry</tr>
+ * </table>
+ * <p>
+ * If no entry path is defined on the URL, the header fields are set from the
+ * property by the base class implementation with the exception of the
+ * content type, which is set to <code>application/java-archive</code> by
+ * the {@link #connect()} method.
+ * <p>
+ * <em>Note that this implementation does only support archives stored in the
+ * JCR Repository, no other contained storage such as </em>file<em> or
+ * </em>http<em> is supported.</em>
+ * <p>
+ * This class is not intended to be subclassed or instantiated by clients.
+ *
+ * @author Felix Meschberger
+ * @version $Rev:$, $Date:$
+ */
+public class JCRJarURLConnection extends JCRURLConnection {
+
+    /** default log category */
+    private static final Log log = LogFactory.getLog(JCRJarURLConnection.class);
+
+    /**
+     * The name of the MIME content type for this connection's content if
+     * no entry path is defined on the URL (value is "application/java-archive").
+     */
+    protected static final String APPLICATION_JAR = "application/java-archive";
+
+    /**
+     * Creates an instance of this class for the given <code>url</code>
+     * supported by the <code>handler</code>.
+     *
+     * @param url The URL to base the connection on.
+     * @param handler The URL handler supporting the given URL.
+     */
+    JCRJarURLConnection(URL url, JCRJarURLHandler handler) {
+        super(url, handler);
+    }
+
+    /**
+     * Returns the path to the entry contained in the archive or
+     * <code>null</code> if the URL contains no entry specification in the
+     * path.
+     */
+    String getEntryPath() {
+        return getFileParts().getEntryPath();
+    }
+
+    /**
+     * Connects to the URL setting the header fields and preparing for the
+     * {@link #getProperty()} and {@link #getInputStream()} methods.
+     * <p>
+     * After calling the base class implemenation to get the basic connection,
+     * the entry is looked for in the archive to set the content type, content
+     * length and last modification time header fields according to the named
+     * entry. If no entry is defined on the URL, only the content type header
+     * field is set to <code>application/java-archive</code>.
+     * <p>
+     * When this method successfully returns, this connection is considered
+     * connected. In case of an exception thrown, the connection is not
+     * connected.
+     *
+     * @throws IOException if an error occurrs retrieving the data property or
+     *      any of the header field value properties or if any other errors
+     *      occurrs. Any cuasing exception is set as the cause of this
+     *      exception.
+     */
+    public synchronized void connect() throws IOException {
+
+        if (!connected) {
+
+            // have the base class connect to get the jar property
+            super.connect();
+
+            // we assume the connection is now (temporarily) connected,
+            // thus calling the getters will not result in a recursive loop
+            Property property = getProperty();
+            String contentType = getContentType();
+            String contentEncoding = getContentEncoding();
+            int contentLength = getContentLength();
+            long lastModified = getLastModified();
+
+            // mark as not connected to not get false positives if the
+            // following code fails
+            connected = false;
+
+            // Get hold of the data
+            try {
+
+                JarInputStream jins = null;
+                try {
+
+                    // try to get the jar input stream, fail if no jar
+                    jins = new JarInputStream(property.getStream());
+
+                    String entryPath = getEntryPath();
+                    if (entryPath != null) {
+
+                        JarEntry entry = findEntry(jins, entryPath);
+
+                        if (entry != null) {
+
+                            contentType = guessContentTypeFromName(entryPath);
+                            if (contentType == null) {
+                                contentType = APPLICATION_OCTET;
+                            }
+
+                            contentLength = (int) entry.getSize();
+                            lastModified = entry.getTime();
+
+                        } else {
+
+                            throw failure("connect", entryPath +
+                                " not contained in jar archive", null);
+
+                        }
+
+                    } else {
+
+                        // replaces the base class defined content type
+                        contentType = APPLICATION_JAR;
+
+                    }
+
+                } finally {
+                    if (jins != null) {
+                        try {
+                            jins.close();
+                        } catch (IOException ignore) {
+                        }
+                    }
+                }
+
+                log.debug("connect: Using atom '" + property.getPath()
+                    + "' with content type '" + contentType + "' for "
+                    + String.valueOf(contentLength) + " bytes");
+
+                // set the fields
+                setContentType(contentType);
+                setContentEncoding(contentEncoding);
+                setContentLength(contentLength);
+                setLastModified(lastModified);
+
+                // mark connection open
+                connected = true;
+
+            } catch (RepositoryException re) {
+
+                throw failure("connect", re.toString(), re);
+
+            }
+        }
+    }
+
+    /**
+     * Returns an input stream that reads from this open connection. If not
+     * entry path is specified in the URL, this method returns the input stream
+     * providing access to the archive as a whole. Otherwise the input stream
+     * returned is a <code>JarInputStream</code> positioned at the start of
+     * the named entry.
+     * <p>
+     * <b>NOTES:</b>
+     * <ul>
+     * <li>Each call to this method returns a new <code>InputStream</code>.
+     * <li>Do not forget to close the return stream when not used anymore for
+     *      the system to be able to free resources.
+     * </ul>
+     * <p>
+     * Calling this method implicitly calls {@link #connect()} to ensure the
+     * connection is open.
+     *
+     * @return The <code>InputStream</code> on the archive or the entry if
+     *      specified.
+     *
+     * @throws IOException if an error occurrs opening the connection through
+     *      {@link #connect()} or creating the <code>InputStream</code> on the
+     *      repository <code>Property</code>.
+     */
+    public InputStream getInputStream() throws IOException {
+
+        // get the input stream on the archive itself - also enforces connect()
+        InputStream ins = super.getInputStream();
+
+        // access the entry in the archive if defined
+        String entryPath = getEntryPath();
+        if (entryPath != null) {
+            // open the jar input stream
+            JarInputStream jins = new JarInputStream(ins);
+
+            // position at the correct entry
+            findEntry(jins, entryPath);
+
+            // return the input stream
+            return jins;
+        }
+
+        // otherwise just return the stream on the archive
+        return ins;
+    }
+
+    //----------- internal helper to find the entry ------------------------
+
+    /**
+     * 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.
+     */
+    static JarEntry findEntry(JarInputStream zins, String path)
+        throws IOException {
+
+        JarEntry entry = zins.getNextJarEntry();
+        while (entry != null) {
+            if (path.equals(entry.getName())) {
+                return entry;
+            }
+
+            entry = zins.getNextJarEntry();
+        }
+        // invariant : nothing found in the zip matching the path
+
+        return null;
+    }
+}
\ No newline at end of file

Propchange: incubator/jackrabbit/trunk/contrib/classloader/src/main/java/org/apache/jackrabbit/net/JCRJarURLConnection.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: incubator/jackrabbit/trunk/contrib/classloader/src/main/java/org/apache/jackrabbit/net/JCRJarURLConnection.java
------------------------------------------------------------------------------
    svn:keywords = author date id revision url

Added: incubator/jackrabbit/trunk/contrib/classloader/src/main/java/org/apache/jackrabbit/net/JCRJarURLHandler.java
URL: http://svn.apache.org/viewcvs/incubator/jackrabbit/trunk/contrib/classloader/src/main/java/org/apache/jackrabbit/net/JCRJarURLHandler.java?rev=358296&view=auto
==============================================================================
--- incubator/jackrabbit/trunk/contrib/classloader/src/main/java/org/apache/jackrabbit/net/JCRJarURLHandler.java (added)
+++ incubator/jackrabbit/trunk/contrib/classloader/src/main/java/org/apache/jackrabbit/net/JCRJarURLHandler.java Wed Dec 21 06:13:56 2005
@@ -0,0 +1,299 @@
+/*
+ * Copyright 2004-2005 The Apache Software Foundation or its licensors,
+ *                     as applicable.
+ *
+ * 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.apache.jackrabbit.net;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLConnection;
+
+import javax.jcr.Session;
+
+/**
+ * The <code>JCRJarURLHandler</code> is the <code>URLStreamHandler</code> for
+ * Java Archive URLs for archives from a JCR Repository URLs (JCRJar URL). The
+ * scheme for such ULRs will be <code>jar</code> while the file part of the URL
+ * has the scheme <code>jcr</code>.
+ * <p>
+ * JCRJar URLs have not been standardized yet and may only be created in the
+ * context of an existing <code>Session</code>. Therefore this handler is not
+ * globally available and JCR Repository URLs may only be created through the
+ * factory methods in the {@link org.apache.jackrabbit.net.URLFactory} class.
+ * <p>
+ * This class is not intended to be subclassed or instantiated by clients.
+ *
+ * @author Felix Meschberger
+ * @version $Rev:$, $Date:$
+ *
+ * @see org.apache.jackrabbit.net.JCRJarURLConnection
+ * @see org.apache.jackrabbit.net.URLFactory
+ * @see org.apache.jackrabbit.net.URLFactory#createJarURL(Session, String, String)
+ */
+class JCRJarURLHandler extends JCRURLHandler {
+
+    /**
+     * Creates an instance of this handler class.
+     *
+     * @param session The <code>Session</code> supporting this handler. This
+     *      must not be <code>null</code>.
+     *
+     * @throws NullPointerException if <code>session</code> is <code>null</code>.
+     */
+    JCRJarURLHandler(Session session) {
+        super(session);
+    }
+
+    //---------- URLStreamHandler abstracts ------------------------------------
+
+    /**
+     * Gets a connection object to connect to an JCRJar URL.
+     *
+     * @param url The JCRJar URL to connect to.
+     *
+     * @return An instance of the {@link JCRJarURLConnection} class.
+     *
+     * @see JCRJarURLConnection
+     */
+    protected URLConnection openConnection(URL url) {
+        return new JCRJarURLConnection(url, this);
+    }
+
+    /**
+     * Parses the string representation of a <code>URL</code> into a
+     * <code>URL</code> object.
+     * <p>
+     * If there is any inherited context, then it has already been copied into
+     * the <code>URL</code> argument.
+     * <p>
+     * The <code>parseURL</code> method of <code>URLStreamHandler</code>
+     * parses the string representation as if it were an <code>http</code>
+     * specification. Most URL protocol families have a similar parsing. A
+     * stream protocol handler for a protocol that has a different syntax must
+     * override this routine.
+     *
+     * @param url the <code>URL</code> to receive the result of parsing the
+     *            spec.
+     * @param spec the <code>String</code> representing the URL that must be
+     *            parsed.
+     * @param start the character index at which to begin parsing. This is just
+     *            past the '<code>:</code>' (if there is one) that specifies
+     *            the determination of the protocol name.
+     * @param limit the character position to stop parsing at. This is the end
+     *            of the string or the position of the "<code>#</code>"
+     *            character, if present. All information after the sharp sign
+     *            indicates an anchor.
+     */
+    protected void parseURL(URL url, String spec, int start, int limit) {
+        // protected void parseURL(URL url, String s, int i, int j)
+
+        String file = null;
+        String ref = null;
+
+        // split the reference and file part
+        int hash = spec.indexOf('#', limit);
+        boolean emptyFile = hash == start;
+        if (hash > -1) {
+            ref = spec.substring(hash + 1, spec.length());
+            if (emptyFile) {
+                file = url.getFile();
+            }
+        }
+
+        boolean isSpecAbsolute = spec.substring(0, 4).equalsIgnoreCase("jar:");
+        spec = spec.substring(start, limit);
+
+        if (isSpecAbsolute) {
+
+            // get the file part from the absolute spec
+            file = parseAbsoluteSpec(spec);
+
+        } else if (!emptyFile) {
+
+            // build the file part from the url and relative spec
+            file = parseContextSpec(url, spec);
+
+            // split archive and entry names
+            int bangSlash = indexOfBangSlash(file);
+            String archive = file.substring(0, bangSlash);
+            String entry = file.substring(bangSlash);
+
+            // collapse /../, /./ and //
+            entry = canonizeString(entry);
+
+            file = archive + entry;
+
+        }
+
+        setURL(url, "jar", "", -1, null, null, file, null, ref);
+    }
+
+    //---------- internal -----------------------------------------------------
+
+    /**
+     * Finds the position of the bang slash (!/) in the file part of the URL.
+     */
+    static int indexOfBangSlash(String file) {
+
+        for (int i = file.length(); (i = file.lastIndexOf('!', i)) != -1; i--) {
+            if (i != file.length() - 1 && file.charAt(i + 1) == '/') {
+                return i + 1;
+            }
+        }
+
+        return -1;
+    }
+
+    /**
+     * Parses the URL spec and checks whether it contains a bang slash and
+     * whether it would get a valid URL. Returns the same value if everything is
+     * fine else a <code>NullPointerException</code> is thrown.
+     *
+     * @param spec The URL specification to check.
+     * @return The <code>spec</code> if everything is ok.
+     * @throws NullPointerException if either no bang slash is contained in the
+     *             spec or if the spec without the bang slash part would not be
+     *             a valid URL.
+     */
+    private String parseAbsoluteSpec(String spec) {
+
+        // find and check bang slash
+        int bangSlash = indexOfBangSlash(spec);
+        if (bangSlash == -1) {
+            throw new NullPointerException("no !/ in spec");
+        }
+
+        try {
+
+            String testSpec = spec.substring(0, bangSlash - 1);
+            URI uri = new URI(testSpec);
+
+            // verify the scheme is the JCR Repository Scheme
+            if (!URLFactory.REPOSITORY_SCHEME.equals(uri.getScheme())) {
+                throw new URISyntaxException(testSpec,
+                    "Unsupported Scheme " + uri.getScheme(), 0);
+            }
+
+        } catch (URISyntaxException use) {
+
+            throw new NullPointerException("invalid url: " + spec + " (" + use
+                + ")");
+
+        }
+
+        return spec;
+    }
+
+    /**
+     * Merges the specification and the file part of the URL respecting the bang
+     * slashes. If the specification starts with a slash, it is regarded as a
+     * complete path of a archive entry and replaces an existing archive entry
+     * specification in the url. Examples :<br>
+     * <table>
+     * <tr>
+     * <th align="left">file
+     * <th align="left">spec
+     * <th align="left">result
+     * <tr>
+     * <td>/some/file/path.jar!/
+     * <td>/some/entry/path
+     * <td>/some/file/path.jar!/some/entry/path
+     * <tr>
+     * <td>/some/file/path.jar!/some/default
+     * <td>/some/entry/path
+     * <td>/some/file/path.jar!/some/entry/path </table>
+     * <p>
+     * If the specification is not absolutes it replaces the last file name part
+     * if the file name does not end with a slash. Examples :<br>
+     * <table>
+     * <tr>
+     * <th align="left">file
+     * <th align="left">spec
+     * <th align="left">result
+     * <tr>
+     * <td>/some/file/path.jar!/
+     * <td>/some/entry/path
+     * <td>/some/file/path.jar!/some/entry/path
+     * <tr>
+     * <td>/some/file/path.jar!/some/default
+     * <td>/some/entry/path
+     * <td>/some/file/path.jar!/some/entry/path </table>
+     *
+     * @param url The <code>URL</code> whose file part is used
+     * @param spec The specification to merge with the file part
+     * @throws NullPointerException If the specification starts with a slash and
+     *             the URL does not contain a slash bang or if the specification
+     *             does not start with a slash and the file part of the URL does
+     *             is not an absolute file path.
+     */
+    private String parseContextSpec(URL url, String spec) {
+
+        // spec is relative to this file
+        String file = url.getFile();
+
+        // if the spec is absolute path, it is an absolute entry spec
+        if (spec.startsWith("/")) {
+
+            // assert the bang slash in the original URL
+            int bangSlash = indexOfBangSlash(file);
+            if (bangSlash == -1) {
+                throw new NullPointerException("malformed context url:" + url
+                    + ": no !/");
+            }
+
+            // remove bang slash part from the original file
+            file = file.substring(0, bangSlash);
+        }
+
+        // if the file is not a directory and spec is a relative file path
+        if (!file.endsWith("/") && !spec.startsWith("/")) {
+
+            // find the start of the file name in the url file path
+            int lastSlash = file.lastIndexOf('/');
+            if (lastSlash == -1) {
+                throw new NullPointerException("malformed context url:" + url);
+            }
+
+            // cut off the file name from the URL file path
+            file = file.substring(0, lastSlash + 1);
+        }
+
+        // concat file part and the spec now
+        return file + spec;
+    }
+
+    public String canonizeString(String s) {
+        int i = 0;
+        int k = s.length();
+        while ((i = s.indexOf("/../")) >= 0)
+            if ((k = s.lastIndexOf('/', i - 1)) >= 0)
+                s = s.substring(0, k) + s.substring(i + 3);
+            else
+                s = s.substring(i + 3);
+        while ((i = s.indexOf("/./")) >= 0)
+            s = s.substring(0, i) + s.substring(i + 2);
+        while (s.endsWith("/..")) {
+            int j = s.indexOf("/..");
+            int l;
+            if ((l = s.lastIndexOf('/', j - 1)) >= 0)
+                s = s.substring(0, l + 1);
+            else
+                s = s.substring(0, j);
+        }
+        if (s.endsWith("/.")) s = s.substring(0, s.length() - 1);
+        return s;
+    }
+}
\ No newline at end of file

Propchange: incubator/jackrabbit/trunk/contrib/classloader/src/main/java/org/apache/jackrabbit/net/JCRJarURLHandler.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: incubator/jackrabbit/trunk/contrib/classloader/src/main/java/org/apache/jackrabbit/net/JCRJarURLHandler.java
------------------------------------------------------------------------------
    svn:keywords = author date id revision url