You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ro...@apache.org on 2017/11/07 09:46:56 UTC
[sling-org-apache-sling-jcr-classloader] 08/18: SLING-1316 -
Include jackrabbit classloader code to adjust it for Sling needs - code
import with first changes.
This is an automated email from the ASF dual-hosted git repository.
rombert pushed a commit to annotated tag org.apache.sling.jcr.classloader-3.1.0
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-jcr-classloader.git
commit c21156ab23710e3ea75712d43b028b779f567291
Author: Carsten Ziegeler <cz...@apache.org>
AuthorDate: Mon Jan 25 12:43:22 2010 +0000
SLING-1316 - Include jackrabbit classloader code to adjust it for Sling needs - code import with first changes.
git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/bundles/jcr/classloader@902794 13f79535-47bb-0310-9956-ffa450edef68
---
pom.xml | 11 -
.../classloader/internal/ClassLoaderResource.java | 414 +++++++++++
.../jcr/classloader/internal/ClassPathEntry.java | 227 ++++++
.../internal/DynamicRepositoryClassLoader.java | 660 +++++++++++++++++
.../internal/RepositoryClassLoaderFacade.java | 1 -
.../internal/URLRepositoryClassLoader.java | 795 +++++++++++++++++++++
.../sling/jcr/classloader/internal/Util.java | 166 +++++
.../jcr/classloader/internal/net/FileParts.java | 312 ++++++++
.../internal/net/JCRJarURLConnection.java | 289 ++++++++
.../classloader/internal/net/JCRJarURLHandler.java | 298 ++++++++
.../classloader/internal/net/JCRURLConnection.java | 778 ++++++++++++++++++++
.../classloader/internal/net/JCRURLHandler.java | 147 ++++
.../jcr/classloader/internal/net/URLFactory.java | 99 +++
13 files changed, 4185 insertions(+), 12 deletions(-)
diff --git a/pom.xml b/pom.xml
index 371a03f..5da1fbc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -59,11 +59,6 @@
</Bundle-Category>
<Private-Package>
org.apache.sling.jcr.classloader.internal.*,
- org.apache.jackrabbit;
- org.apache.jackrabbit.classloader;
- org.apache.jackrabbit.name;
- org.apache.jackrabbit.net;
- org.apache.jackrabbit.util;split-package:=merge-first
</Private-Package>
</instructions>
</configuration>
@@ -115,12 +110,6 @@
<artifactId>junit</artifactId>
</dependency>
<dependency>
- <groupId>commons-collections</groupId>
- <artifactId>commons-collections</artifactId>
- <version>3.2.1</version>
- <scope>provided</scope>
- </dependency>
- <dependency>
<groupId>org.osgi</groupId>
<artifactId>org.osgi.core</artifactId>
</dependency>
diff --git a/src/main/java/org/apache/sling/jcr/classloader/internal/ClassLoaderResource.java b/src/main/java/org/apache/sling/jcr/classloader/internal/ClassLoaderResource.java
new file mode 100644
index 0000000..73671bf
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/classloader/internal/ClassLoaderResource.java
@@ -0,0 +1,414 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * 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.sling.jcr.classloader.internal;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.security.cert.Certificate;
+import java.util.Date;
+import java.util.jar.Manifest;
+
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+
+import org.apache.sling.jcr.classloader.internal.net.URLFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * The <code>ClassLoaderResource</code> class represents a resource looked up
+ * by the {@link ClassPathEntry}s of the {@link URLRepositoryClassLoader}.
+ */
+class ClassLoaderResource {
+
+ /** default log category */
+ private static final Logger log =
+ LoggerFactory.getLogger(ClassLoaderResource.class);
+
+ /**
+ * The class path entry which loaded this class loader resource
+ */
+ private final ClassPathEntry pathEntry;
+
+ /**
+ * The name of this resource.
+ */
+ private final String name;
+
+ /**
+ * The repository property providing the resource's contents. This may be
+ * <code>null</code> if the resource was loaded from a JAR/ZIP archive.
+ */
+ private final Property resProperty;
+
+ /**
+ * The class optionally loaded/defined through this resource.
+ *
+ * @see #getLoadedClass()
+ * @see #setLoadedClass(Class)
+ */
+ private Class<?> loadedClass;
+
+ /**
+ * The time in milliseconds at which this resource has been loaded from
+ * the repository.
+ */
+ private final long loadTime;
+
+ /**
+ * Flag indicating that this resource has already been checked for expiry
+ * and whether it is actually expired.
+ *
+ * @see #isExpired()
+ */
+ private boolean expired;
+
+ /**
+ * Creates an instance of this class for the class path entry.
+ *
+ * @param pathEntry The {@link ClassPathEntry} of the code source of this
+ * class loader resource.
+ * @param name The path name of this resource.
+ * @param resProperty The <code>Property</code>providing the content's of
+ * this resource. This may be <code>null</code> if the resource
+ * was loaded from an JAR or ZIP archive.
+ */
+ /* package */ ClassLoaderResource(ClassPathEntry pathEntry, String name,
+ Property resProperty) {
+ this.pathEntry = pathEntry;
+ this.name = name;
+ this.resProperty = resProperty;
+ this.loadTime = System.currentTimeMillis();
+ }
+
+ /**
+ * Returns the {@link ClassPathEntry} which loaded this resource.
+ */
+ protected ClassPathEntry getClassPathEntry() {
+ return pathEntry;
+ }
+
+ /**
+ * Returns the name of this resource. This is the name used to find the
+ * resource, for example the class name or the properties file path.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the <code>Property</code> with which this resource is created.
+ */
+ protected Property getProperty() {
+ return resProperty;
+ }
+
+ /**
+ * Returns the time in milliseconds at which this resource has been loaded
+ */
+ protected long getLoadTime() {
+ return loadTime;
+ }
+
+ /**
+ * Returns the URL to access this resource, for example a JCR or a JAR URL.
+ * If the URL cannot be created from the resource data, <code>null</code> is
+ * returned.
+ */
+ public URL getURL() {
+ try {
+ return URLFactory.createURL(getClassPathEntry().session, getPath());
+ } catch (Exception e) {
+ log.warn("getURL: Cannot getURL for " + getPath(), e);
+ }
+ return null;
+ }
+
+ /**
+ * Returns the URL to the code source of this entry. If there is no code
+ * source available, <code>null</code> is returned.
+ * <p>
+ * This base class implementation returns the result of calling
+ * {@link ClassPathEntry#toURL()} on the class path entry from which this
+ * resource was loaded.
+ */
+ public URL getCodeSourceURL() {
+ return getClassPathEntry().toURL();
+ }
+
+ /**
+ * Returns an <code>InputStream</code> to read from the resource.
+ * <p>
+ * This base class implementation returns the result of calling the
+ * <code>getStream()</code> method on the resource's property or
+ * <code>null</code> if the property is not set.
+ */
+ public InputStream getInputStream() throws RepositoryException {
+ return (getProperty() != null) ? getProperty().getStream() : null;
+ }
+
+ /**
+ * Returns the size of the resource or -1 if the size cannot be found out.
+ * <p>
+ * This base class implementation returns the result of calling the
+ * <code>getLength()</code> method on the resource's property or -1 if
+ * the property is not set.
+ *
+ * @throws RepositoryException If an error occurrs trying to find the length
+ * of the property.
+ */
+ public int getContentLength() throws RepositoryException {
+ return (getProperty() != null) ? (int) getProperty().getLength() : -1;
+ }
+
+ /**
+ * Returns the path of the property containing the resource.
+ * <p>
+ * This base class implementation returns the absolute path of the
+ * resource's property. If the property is not set or if an error occurrs
+ * accesing the property's path, the concatentation of the class path
+ * entry's path and the resource's name is returned.
+ */
+ public String getPath() {
+ if (getProperty() != null) {
+ try {
+ return getProperty().getPath();
+ } catch (RepositoryException re) {
+ // fallback
+ log.warn("getPath: Cannot retrieve path of entry " + getName(),
+ re);
+ }
+ }
+
+ // fallback if no resource property or an error accessing the path of
+ // the property
+ return getSafePath();
+ }
+
+ /**
+ * Returns the path of the property containing the resource by appending
+ * the {@link #getName() name} to the path of the class path entry to which
+ * this resource belongs. This path need not necessairily be the same as
+ * the {@link #getProperty() path of the property} but will always succeed
+ * as there is no repository access involved.
+ */
+ protected String getSafePath() {
+ return getClassPathEntry().getPath() + getName();
+ }
+
+ /**
+ * Returns the time of the the last modification of the resource or -1 if
+ * the last modification time cannot be evaluated.
+ * <p>
+ * This base class implementation returns the result of calling the
+ * {@link Util#getLastModificationTime(Property)} method on the resource's
+ * property if not <code>null</code>. In case of an error or if the
+ * property is <code>null</code>, -1 is returned.
+ */
+ public long getLastModificationTime() {
+ if (getProperty() != null) {
+ try {
+ return Util.getLastModificationTime(getProperty());
+ } catch (RepositoryException re) {
+ log.info("getLastModificationTime of resource property", re);
+ }
+ }
+
+ // cannot find the resource modification time, use epoch
+ return -1;
+ }
+
+ /**
+ * Returns the resource as an array of bytes
+ */
+ public byte[] getBytes() throws IOException, RepositoryException {
+ InputStream in = null;
+ byte[] buf = null;
+
+ log.debug("getBytes");
+
+ try {
+ in = getInputStream();
+ log.debug("getInputStream() returned {}", in);
+
+ int length = getContentLength();
+ log.debug("getContentLength() returned {}", new Integer(length));
+
+ if (length >= 0) {
+
+ buf = new byte[length];
+ for (int read; length > 0; length -= read) {
+ read = in.read(buf, buf.length - length, length);
+ if (read == -1) {
+ throw new IOException("unexpected EOF");
+ }
+ }
+
+ } else {
+
+ buf = new byte[1024];
+ int count = 0;
+ int read;
+
+ // read enlarging buffer
+ while ((read = in.read(buf, count, buf.length - count)) != -1) {
+ count += read;
+ if (count >= buf.length) {
+ byte buf1[] = new byte[count * 2];
+ System.arraycopy(buf, 0, buf1, 0, count);
+ buf = buf1;
+ }
+ }
+
+ // resize buffer if too big
+ if (count != buf.length) {
+ byte buf1[] = new byte[count];
+ System.arraycopy(buf, 0, buf1, 0, count);
+ buf = buf1;
+ }
+
+ }
+
+ } finally {
+
+ if (in != null) {
+ try {
+ in.close();
+ } catch (IOException ignore) {
+ }
+ }
+
+ }
+
+ return buf;
+ }
+
+ /**
+ * Returns the manifest from the jar file for this class resource. If this
+ * resource is not from a jar file, the method returns <code>null</code>,
+ * which is what the default implementation does.
+ */
+ public Manifest getManifest() {
+ return null;
+ }
+
+ /**
+ * Returns the certificates from the jar file for this class resource. If
+ * this resource is not from a jar file, the method returns
+ * <code>null</code>, which is what the default implementation does.
+ */
+ public Certificate[] getCertificates() {
+ return null;
+ }
+
+ /**
+ * Returns the <code>Property</code> which is used to check whether this
+ * resource is expired or not.
+ * <p>
+ * This base class method returns the same property as returned by the
+ * {@link #getProperty()} method. This method may be overwritten by
+ * implementations as appropriate.
+ *
+ * @see #isExpired()
+ */
+ protected Property getExpiryProperty() {
+ return getProperty();
+ }
+
+ /**
+ * Returns <code>true</code> if the last modification date of the expiry
+ * property of this resource is loaded is later than the time at which this
+ * resource has been loaded. If the last modification time of the expiry
+ * property cannot be calculated or if an error occurrs checking the expiry
+ * propertiy's last modification time, <code>true</code> is returned.
+ */
+ public boolean isExpired() {
+ if (!expired) {
+ // creation time of version if loaded now
+ long currentPropTime = 0;
+ Property prop = getExpiryProperty();
+ if (prop != null) {
+ try {
+ currentPropTime = Util.getLastModificationTime(prop);
+ } catch (RepositoryException re) {
+ // cannot get last modif time from property, use current time
+ log.debug("expireResource: Cannot get current version for "
+ + toString() + ", will expire", re);
+ currentPropTime = System.currentTimeMillis();
+ }
+ }
+
+ // creation time of version currently loaded
+ long loadTime = getLoadTime();
+
+ // expire if a new version would be loaded
+ expired = currentPropTime > loadTime;
+ if (expired && log.isDebugEnabled()) {
+ log.debug(
+ "expireResource: Resource created {} superceded by version created {}",
+ new Date(loadTime), new Date(currentPropTime));
+ }
+ }
+
+ return expired;
+ }
+
+ /**
+ * Returns the class which was loaded through this resource. It is expected
+ * that the class loader sets the class which was loaded through this
+ * resource by calling the {@link #setLoadedClass(Class)} method. If this
+ * class was not used to load a class or if the class loader failed to
+ * set the class loaded through this resoource, this method will return
+ * <code>null</code>.
+ *
+ * @return The class loaded through this resource, which may be
+ * <code>null</code> if this resource was never used to load a class
+ * or if the loader failed to set class through the
+ * {@link #setLoadedClass(Class)} method.
+ *
+ * @see #setLoadedClass(Class)
+ */
+ public Class<?> getLoadedClass() {
+ return loadedClass;
+ }
+
+ /**
+ * Sets the class which was loaded through this resource. This method does
+ * not check, whether it is plausible that this class was actually loaded
+ * from this resource, nor does this method check whether the class object
+ * is <code>null</code> or not.
+ *
+ * @param loadedClass The class to be loaded.
+ */
+ public void setLoadedClass(Class<?> loadedClass) {
+ this.loadedClass = loadedClass;
+ }
+
+ /**
+ * Returns the <code>String</code> representation of this resource.
+ */
+ public String toString() {
+ final StringBuilder buf = new StringBuilder(getClass().getName());
+ buf.append(": path=");
+ buf.append(getSafePath());
+ buf.append(", name=");
+ buf.append(getName());
+ return buf.toString();
+ }
+}
diff --git a/src/main/java/org/apache/sling/jcr/classloader/internal/ClassPathEntry.java b/src/main/java/org/apache/sling/jcr/classloader/internal/ClassPathEntry.java
new file mode 100644
index 0000000..3f79345
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/classloader/internal/ClassPathEntry.java
@@ -0,0 +1,227 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * 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.sling.jcr.classloader.internal;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.security.AccessControlException;
+
+import javax.jcr.PathNotFoundException;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.apache.sling.jcr.classloader.internal.net.URLFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The <code>ClassPathEntry</code> class encapsulates entries in the class path
+ * of the {@link DynamicRepositoryClassLoader}. The main task is to retrieve
+ * {@link ClassLoaderResource} instances for classes or resources to load from it.
+ * <p>
+ * This implementation is not currently integrated with Java security. That is
+ * protection domains and security managers are not supported yet.
+ * <p>
+ * This class is not intended to be subclassed or instantiated by clients.
+ */
+public final class ClassPathEntry {
+
+ /** default logging */
+ private static final Logger log =
+ LoggerFactory.getLogger(ClassPathEntry.class);
+
+ /** The session assigned to this class path entry */
+ protected final Session session;
+
+ /** The path to the item of this class path entry */
+ protected final String path;
+
+ /** The base URL for the class path entry to later construct resource URLs */
+ protected URL baseURL;
+
+ //---------- construction --------------------------------------------------
+
+ /**
+ * Creates an instance of the <code>ClassPathEntry</code> assigning the
+ * session and path.
+ *
+ * @param session The <code>Session</code> to access the Repository.
+ * @param path The path of the class path entry, this is either the
+ * path of a node containing a jar archive or is the path
+ * of the root of a hierarchy to look up resources in.
+ */
+ protected ClassPathEntry(Session session, String path) {
+ this.path = path;
+ this.session = session;
+ }
+
+ /**
+ * Clones this instance of the <code>ClassPathEntry</code> setting the
+ * path and session to the same value as the base instance.
+ * <p>
+ * Note that this constructor does not duplicate the session from the base
+ * instance.
+ *
+ * @param base The <code>ClassPathEntry</code> from which to copy the path
+ * and the session.
+ */
+ protected ClassPathEntry(ClassPathEntry base) {
+ this.path = base.path;
+ this.session = base.session;
+ this.baseURL = base.baseURL;
+ }
+
+ /**
+ * Returns an instance of the <code>ClassPathEntry</code> class. This
+ * instance will be a subclass correctly handling the type (directory or
+ * jar archive) of class path entry is to be created.
+ * <p>
+ * If the path given has a trailing slash, it is taken as a directory root
+ * else the path is first tested, whether it contains an archive. If not
+ * the path is treated as a directory.
+ *
+ * @param session The <code>Session</code> to access the Repository.
+ * @param path The path of the class path entry, this is either the
+ * path of a node containing a jar archive or is the path
+ * of the root of a hierharchy to look up resources in.
+ *
+ * @return An initialized <code>ClassPathEntry</code> instance for the
+ * path or <code>null</code> if an error occurred creating the
+ * instance.
+ */
+ static ClassPathEntry getInstance(Session session, String path) {
+
+ // check we can access the path, don't care about content now
+ try {
+ session.checkPermission(path, "read");
+ } catch (AccessControlException ace) {
+ log.warn(
+ "getInstance: Access denied reading from {}, ignoring entry",
+ path);
+ return null;
+ } catch (RepositoryException re) {
+ log.error("getInstance: Cannot check permission to " + path, re);
+ }
+
+ if (!path.endsWith("/")) {
+ // assume the path designates a directory
+ // append trailing slash now
+ path += "/";
+ }
+
+ // we assume a directory class path entry, but we might have to check
+ // whether the path refers to a node or not. On the other hande, this
+ // class path entry will not be usable anyway if not, user beware :-)
+
+ return new ClassPathEntry(session, path);
+ }
+
+ /**
+ * Returns the path on which this <code>ClassPathEntry</code> is based.
+ */
+ public String getPath() {
+ return path;
+ }
+
+ /**
+ * Returns this <code>ClassPathEntry</code> represented as an URL to be
+ * used in a list of URLs to further work on. If there is a problem creating
+ * the URL for this instance, <code>null</code> is returned instead.
+ */
+ public URL toURL() {
+ if (baseURL == null) {
+ try {
+ baseURL = URLFactory.createURL(session, path);
+ } catch (MalformedURLException mue) {
+ log.warn("DirectoryClassPathEntry: Creating baseURl for " +
+ path, mue);
+ }
+ }
+
+ return baseURL;
+ }
+
+ /**
+ * Returns a {@link ClassLoaderResource} for the named resource if it
+ * can befound below this directory root identified by the path given
+ * at construction time. Note that if the page would exist but does
+ * either not contain content or 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 dashes 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 {
+ final Property prop = Util.getProperty(session.getItem(path + name));
+ if (prop != null) {
+ return new ClassLoaderResource(this, name, prop);
+ }
+
+ log.debug("getResource: resource {} not found below {} ", name,
+ path);
+
+ } catch (PathNotFoundException pnfe) {
+
+ log.debug("getResource: Classpath entry {} does not have resource {}",
+ path, name);
+
+ } catch (RepositoryException cbe) {
+
+ log.warn("getResource: problem accessing the resource {} below {}",
+ new Object[] { name, path }, cbe);
+
+ }
+ // invariant : no page or problem accessing the page
+
+ return null;
+ }
+
+ /**
+ * Returns a <code>ClassPathEntry</code> with the same configuration as
+ * this <code>ClassPathEntry</code>.
+ * <p>
+ * Becase the <code>DirectoryClassPathEntry</code> class does not have
+ * internal state, this method returns this instance to be used as
+ * the "copy".
+ */
+ ClassPathEntry copy() {
+ return this;
+ }
+
+ /**
+ * @see Object#toString()
+ */
+ public String toString() {
+ StringBuilder buf = new StringBuilder(super.toString());
+ buf.append(": path: ");
+ buf.append(path);
+ buf.append(", user: ");
+ buf.append(session.getUserID());
+ return buf.toString();
+ }
+
+ //----------- internal helper ----------------------------------------------
+
+}
diff --git a/src/main/java/org/apache/sling/jcr/classloader/internal/DynamicRepositoryClassLoader.java b/src/main/java/org/apache/sling/jcr/classloader/internal/DynamicRepositoryClassLoader.java
new file mode 100644
index 0000000..35df60a
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/classloader/internal/DynamicRepositoryClassLoader.java
@@ -0,0 +1,660 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * 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.sling.jcr.classloader.internal;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.observation.Event;
+import javax.jcr.observation.EventIterator;
+import javax.jcr.observation.EventListener;
+import javax.jcr.observation.ObservationManager;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * The <code>DynamicRepositoryClassLoader</code> class extends the
+ * {@link org.apache.sling.jcr.classloader.internal.URLRepositoryClassLoader} and provides the
+ * functionality to load classes and resources from the JCR Repository.
+ * Additionally, this class supports the notion of getting 'dirty', which means,
+ * that if a resource loaded through this class loader has been modified in the
+ * Repository, this class loader marks itself dirty, which flag can get
+ * retrieved. This helps the user of this class loader to decide on whether to
+ * {@link #reinstantiate(Session, ClassLoader) reinstantiate} it or continue
+ * using this class loader.
+ * <p>
+ * When a user of the class loader recognizes an instance to be dirty, it can
+ * easily be reinstantiated with the {@link #reinstantiate} method. This
+ * reinstantiation will also rebuild the internal real class path from the same
+ * list of path patterns as was used to create the internal class path for the
+ * original class loader. The resulting internal class path need not be the
+ * same, though.
+ * <p>
+ * As an additional feature the class loaders provides the functionality for
+ * complete reconfiguration of the list of path patterns defined at class loader
+ * construction time through the {@link #reconfigure(String[])} method. This
+ * reconfiguration replaces the internal class path with a new one built from
+ * the new path list and also replaces that path list. Reinstantiating a
+ * reconfigured class loader gets a class loader containing the same path list
+ * as the original class loader had after reconfiguration. That is the original
+ * configuration is lost. While reconfiguration is not able to throw away
+ * classes already loaded, it will nevertheless mark the class loader dirty, if
+ * any classes have already been loaded through it.
+ * <p>
+ * This class is not intended to be extended by clients.
+ */
+public class DynamicRepositoryClassLoader
+ extends URLRepositoryClassLoader
+ implements EventListener {
+
+ /** default log category */
+ private final Logger log =
+ LoggerFactory.getLogger(this.getClass().getName());
+
+ /**
+ * Cache of resources used to check class loader expiry. The map is indexed
+ * by the paths of the expiry properties of the cached resources. This map
+ * is not complete in terms of resources which have been loaded through this
+ * class loader. That is for resources loaded through an archive class path
+ * entry, only one of those resources (the last one loaded) is kept in this
+ * cache, while the others are ignored.
+ *
+ * @see #onEvent(EventIterator)
+ * @see #findClassLoaderResource(String)
+ */
+ private Map modTimeCache;
+
+ /**
+ * Flag indicating whether there are loaded classes which have later been
+ * expired (e.g. invalidated or modified)
+ */
+ private boolean dirty;
+
+ /**
+ * The list of repositories added through either the {@link #addURL} or the
+ * {@link #addHandle} method.
+ */
+ private ClassPathEntry[] addedRepositories;
+
+ private EventListener[] proxyListeners;
+
+ /**
+ * Creates a <code>DynamicRepositoryClassLoader</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 DynamicRepositoryClassLoader(Session session,
+ String[] classPath, ClassLoader parent) {
+
+ // initialize the super class with an empty class path
+ super(session, classPath, parent);
+
+ // set fields
+ dirty = false;
+ modTimeCache = new HashMap();
+
+ // register with observation service and path pattern list
+ registerModificationListener();
+
+ log.debug("DynamicRepositoryClassLoader: {} ready", this);
+ }
+
+ /**
+ * Creates a <code>DynamicRepositoryClassLoader</code> with the same
+ * configuration as the given <code>DynamicRepositoryClassLoader</code>.
+ * This constructor is used by the {@link #reinstantiate} method.
+ * <p>
+ * Before returning from this constructor the <code>old</code> class loader
+ * is destroyed and may not be used any more.
+ *
+ * @param session The session to associate with this class loader.
+ * @param old The <code>DynamicRepositoryClassLoader</code> to copy the
+ * cofiguration from.
+ * @param parent The parent <code>ClassLoader</code>, which may be
+ * <code>null</code>.
+ */
+ private DynamicRepositoryClassLoader(Session session,
+ DynamicRepositoryClassLoader old, ClassLoader parent) {
+
+ // initialize the super class with an empty class path
+ super(session, old.getPaths(), parent);
+
+ // set the configuration and fields
+ dirty = false;
+ modTimeCache = new HashMap();
+
+ // create a repository from the handles - might get a different one
+ setRepository(resetClassPathEntries(old.getRepository()));
+ setAddedRepositories(resetClassPathEntries(old.getAddedRepositories()));
+ buildRepository();
+
+ // register with observation service and path pattern list
+ registerModificationListener();
+
+ // finally finalize the old class loader
+ old.destroy();
+
+ log.debug(
+ "DynamicRepositoryClassLoader: Copied {}. Do not use that anymore",
+ old);
+ }
+
+ /**
+ * 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;
+ }
+
+ // remove ourselves as listeners from other places
+ unregisterListener();
+
+ addedRepositories = null;
+
+ super.destroy();
+ }
+
+ //---------- reload support ------------------------------------------------
+
+ /**
+ * Checks whether this class loader already loaded the named resource and
+ * would load another version if it were instructed to do so. As a side
+ * effect the class loader sets itself dirty in this case.
+ * <p>
+ * Calling this method yields the same result as calling
+ * {@link #shouldReload(String, boolean)} with the <code>force</code>
+ * argument set to <code>false</code>.
+ *
+ * @param name The name of the resource to check.
+ *
+ * @return <code>true</code> if the resource is loaded and reloading would
+ * take another version than currently loaded.
+ *
+ * @see #isDirty
+ */
+ public synchronized boolean shouldReload(String name) {
+ return shouldReload(name, false);
+ }
+
+ /**
+ * Checks whether this class loader already loaded the named resource and
+ * whether the class loader should be set dirty depending on the
+ * <code>force</code> argument. If the argument is <code>true</code>, the
+ * class loader is marked dirty and <code>true</code> is returned if the
+ * resource has been loaded, else the loaded resource is checked for expiry
+ * and the class loader is only set dirty if the loaded resource has
+ * expired.
+ *
+ * @param name The name of the resource to check.
+ * @param force <code>true</code> if the class loader should be marked dirty
+ * if the resource is loaded, else the class loader is only marked
+ * dirty if the resource is loaded and has expired.
+ *
+ * @return <code>true</code> if the resource is loaded and
+ * <code>force</code> is <code>true</code> or if the resource has
+ * expired. <code>true</code> is also returned if this class loader
+ * has already been destroyed.
+ *
+ * @see #isDirty
+ */
+ public synchronized boolean shouldReload(String name, boolean force) {
+ if (isDestroyed()) {
+ log.warn("Classloader already destroyed, reload required");
+ return true;
+ }
+
+ ClassLoaderResource res = getCachedResource(name);
+ if (res != null) {
+ log.debug("shouldReload: Expiring cache entry {}", res);
+ if (force) {
+ log.debug("shouldReload: Forced dirty flag");
+ dirty = true;
+ return true;
+ }
+
+ return expireResource(res);
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns <code>true</code> if any of the loaded classes need reload. Also
+ * sets this class loader dirty. If the class loader is already set dirty
+ * or if this class loader has been destroyed before calling this method,
+ * it returns immediately.
+ *
+ * @return <code>true</code> if any class loader needs to be reinstantiated.
+ *
+ * @see #isDirty
+ */
+ public synchronized boolean shouldReload() {
+
+ // check whether we are already dirty
+ if (isDirty()) {
+ log.debug("shouldReload: Dirty, need reload");
+ return true;
+ }
+
+ // Check whether any class has changed
+ for (Iterator iter = getCachedResources(); iter.hasNext();) {
+ if (expireResource((ClassLoaderResource) iter.next())) {
+ log.debug("shouldReload: Found expired resource, need reload");
+ return true;
+ }
+ }
+
+ // No changes, no need to reload
+ log.debug("shouldReload: No expired resource found, no need to reload");
+ return false;
+ }
+
+ /**
+ * Returns whether the class loader is dirty. This can be the case if any
+ * of the {@link #shouldReload(String)} or {@link #shouldReload()}
+ * methods returned <code>true</code> or if a loaded class has been expired
+ * through the observation.
+ * <p>
+ * This method may also return <code>true</code> if the <code>Session</code>
+ * associated with this class loader is not valid anymore.
+ * <p>
+ * Finally the method always returns <code>true</code> if the class loader
+ * has already been destroyed. Note, however, that a destroyed class loader
+ * cannot be reinstantiated. See {@link #reinstantiate(Session, ClassLoader)}.
+ * <p>
+ * If the class loader is dirty, it should be reinstantiated through the
+ * {@link #reinstantiate} method.
+ *
+ * @return <code>true</code> if the class loader is dirty and needs
+ * reinstantiation.
+ */
+ public boolean isDirty() {
+ return isDestroyed() || dirty || !getSession().isLive();
+ }
+
+ /**
+ * Reinstantiates this class loader. That is, a new ClassLoader with no
+ * loaded class is created with the same configuration as this class loader.
+ * <p>
+ * When the new class loader is returned, this class loader has been
+ * destroyed and may not be used any more.
+ *
+ * @param parent The parent <code>ClassLoader</code> for the reinstantiated
+ * <code>DynamicRepositoryClassLoader</code>, which may be
+ * <code>null</code>.
+ *
+ * @return a new instance with the same configuration as this class loader.
+ *
+ * @throws IllegalStateException if <code>this</code>
+ * {@link DynamicRepositoryClassLoader} has already been destroyed
+ * through the {@link #destroy()} method.
+ */
+ public DynamicRepositoryClassLoader reinstantiate(Session session, ClassLoader parent) {
+ log.debug("reinstantiate: Copying {} with parent {}", this, parent);
+
+ if (isDestroyed()) {
+ throw new IllegalStateException("Destroyed class loader cannot be recreated");
+ }
+
+ // create the new loader
+ DynamicRepositoryClassLoader newLoader =
+ new DynamicRepositoryClassLoader(session, this, parent);
+
+ // return the new loader
+ return newLoader;
+ }
+
+ //---------- URLClassLoader overwrites -------------------------------------
+
+ /**
+ * Reconfigures this class loader with the pattern list. That is the new
+ * pattern list completely replaces the current pattern list. This new
+ * pattern list will also be used later to configure the reinstantiated
+ * class loader.
+ * <p>
+ * If this class loader already has loaded classes using the old, replaced
+ * path list, it is set dirty.
+ * <p>
+ * If this class loader has already been destroyed, this method has no
+ * effect.
+ *
+ * @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.
+ */
+ public void reconfigure(String[] classPath) {
+ if (log.isDebugEnabled()) {
+ log.debug("reconfigure: Reconfiguring the with {}",
+ Arrays.asList(classPath));
+ }
+
+ // whether the loader is destroyed
+ if (isDestroyed()) {
+ log.warn("Cannot reconfigure this destroyed class loader");
+ return;
+ }
+
+ // assign new path and register
+ setPaths(classPath);
+ buildRepository();
+
+ dirty = !hasLoadedResources();
+ log.debug("reconfigure: Class loader is dirty now: {}", (isDirty()
+ ? "yes"
+ : "no"));
+ }
+
+ //---------- RepositoryClassLoader overwrites -----------------------------
+
+ /**
+ * Calls the base class implementation to actually retrieve the resource.
+ * If the resource could be found and provides a non-<code>null</code>
+ * {@link ClassLoaderResource#getExpiryProperty() expiry property}, the
+ * resource is registered with an internal cache to check with when
+ * a repository modification is observed in {@link #onEvent(EventIterator)}.
+ *
+ * @param name The name of the resource to be found
+ *
+ * @return the {@link ClassLoaderResource} found for the name or
+ * <code>null</code> if no such resource is available in the class
+ * path.
+ *
+ * @throws NullPointerException If this class loader has already been
+ * destroyed.
+ */
+ /* package */ ClassLoaderResource findClassLoaderResource(String name) {
+ // call the base class implementation to actually search for it
+ ClassLoaderResource res = super.findClassLoaderResource(name);
+
+ // if it could be found, we register it with the caches
+ if (res != null) {
+ // register the resource in the expiry map, if an appropriate
+ // property is available
+ Property prop = res.getExpiryProperty();
+ if (prop != null) {
+ try {
+ modTimeCache.put(prop.getPath(), res);
+ } catch (RepositoryException re) {
+ log.warn("Cannot register the resource " + res +
+ " for expiry", re);
+ }
+ }
+ }
+
+ // and finally return the resource
+ return res;
+ }
+
+ /**
+ * 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() {
+ super.buildRepository();
+
+ // add added repositories
+ ClassPathEntry[] addedPath = getAddedRepositories();
+ if (addedPath != null && addedPath.length > 0) {
+ ClassPathEntry[] oldClassPath = getRepository();
+ ClassPathEntry[] newClassPath =
+ new ClassPathEntry[oldClassPath.length + addedPath.length];
+
+ System.arraycopy(oldClassPath, 0, newClassPath, 0,
+ oldClassPath.length);
+ System.arraycopy(addedPath, 0, newClassPath, oldClassPath.length,
+ addedPath.length);
+
+ setRepository(newClassPath);
+ }
+ }
+
+ //---------- ModificationListener interface -------------------------------
+
+ /**
+ * Handles a repository item modifcation events checking whether a class
+ * needs to be expired. As a side effect, this method sets the class loader
+ * dirty if a loaded class has been modified in the repository.
+ *
+ * @param events The iterator of repository events to be handled.
+ */
+ public void onEvent(EventIterator events) {
+ while (events.hasNext()) {
+ Event event = events.nextEvent();
+ String path;
+ try {
+ path = event.getPath();
+ } catch (RepositoryException re) {
+ log.warn("onEvent: Cannot get path of event, ignoring", re);
+ continue;
+ }
+
+ log.debug(
+ "onEvent: Item {} has been modified, checking with cache", path);
+
+ ClassLoaderResource resource = (ClassLoaderResource) modTimeCache.get(path);
+ if (resource != null) {
+ log.debug("pageModified: Expiring cache entry {}", resource);
+ expireResource(resource);
+ } else {
+ // might be in not-found cache - remove from there
+ if (event.getType() == Event.NODE_ADDED
+ || event.getType() == Event.PROPERTY_ADDED) {
+ log.debug("pageModified: Clearing not-found cache for possible new class");
+ cleanCache();
+ }
+ }
+
+ }
+ }
+
+ //----------- Object overwrite ---------------------------------------------
+
+ /**
+ * Returns a string representation of this class loader.
+ */
+ public String toString() {
+ if (isDestroyed()) {
+ return super.toString();
+ }
+
+ StringBuilder buf = new StringBuilder(super.toString());
+ buf.append(", dirty: ");
+ buf.append(isDirty());
+ return buf.toString();
+ }
+
+ //---------- internal ------------------------------------------------------
+
+ /**
+ * Sets the list of class path entries to add to the class path after
+ * reconfiguration or reinstantiation.
+ *
+ * @param addedRepositories The list of class path entries to keep for
+ * readdition.
+ */
+ protected void setAddedRepositories(ClassPathEntry[] addedRepositories) {
+ this.addedRepositories = addedRepositories;
+ }
+
+ /**
+ * Returns the list of added class path entries to readd them to the class
+ * path after reconfiguring the class loader.
+ */
+ protected ClassPathEntry[] getAddedRepositories() {
+ return addedRepositories;
+ }
+
+ /**
+ * 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.
+ * <p>
+ * Besides adding the entry to the current class path, it is also added to
+ * the list to be readded after reconfiguration and/or reinstantiation.
+ *
+ * @see #getAddedRepositories()
+ * @see #setAddedRepositories(ClassPathEntry[])
+ */
+ protected void addClassPathEntry(ClassPathEntry cpe) {
+ super.addClassPathEntry(cpe);
+
+ // add the repsitory to the list of added repositories
+ ClassPathEntry[] oldClassPath = getAddedRepositories();
+ ClassPathEntry[] newClassPath = addClassPathEntry(oldClassPath, cpe);
+ setAddedRepositories(newClassPath);
+ }
+
+ /**
+ * Registers this class loader with the observation service to get
+ * information on page updates in the class path and to the path
+ * pattern list to get class path updates.
+ *
+ * @throws NullPointerException if this class loader has already been
+ * destroyed.
+ */
+ private final void registerModificationListener() {
+ log.debug("registerModificationListener: Registering to the observation service");
+
+ final String[] paths = this.getPaths();
+ this.proxyListeners = new EventListener[this.getPaths().length];
+ for(int i=0; i < paths.length; i++ ) {
+ final String path = paths[i];
+ try {
+ final EventListener listener = new ProxyEventListener(this);
+ final ObservationManager om = getSession().getWorkspace().getObservationManager();
+ om.addEventListener(listener, 255, path, true, null, null, false);
+ proxyListeners[i] = listener;
+ } catch (RepositoryException re) {
+ log.error("registerModificationListener: Cannot register " +
+ this + " with observation manager", re);
+ }
+ }
+ }
+
+ /**
+ * Removes this instances registrations from the observation service and
+ * the path pattern list.
+ *
+ * @throws NullPointerException if this class loader has already been
+ * destroyed.
+ */
+ private final void unregisterListener() {
+ log.debug("registerModificationListener: Deregistering from the observation service");
+ if ( this.proxyListeners != null ) {
+ for(final EventListener listener : this.proxyListeners) {
+ if ( listener != null ) {
+ try {
+ final ObservationManager om = getSession().getWorkspace().getObservationManager();
+ om.removeEventListener(listener);
+ } catch (RepositoryException re) {
+ log.error("unregisterListener: Cannot unregister " +
+ this + " from observation manager", re);
+ }
+ }
+ }
+ this.proxyListeners = null;
+ }
+ }
+
+ /**
+ * Checks whether the page backing the resource has been updated with a
+ * version, such that this new version would be used to access the resource.
+ * In this case the resource has expired and the class loader needs to be
+ * set dirty.
+ *
+ * @param resource The <code>ClassLoaderResource</code> to check for
+ * expiry.
+ */
+ private boolean expireResource(ClassLoaderResource resource) {
+
+ // check whether the resource is expired (only if a class has been loaded)
+ boolean exp = resource.getLoadedClass() != null && resource.isExpired();
+
+ // update dirty flag accordingly
+ dirty |= exp;
+ log.debug("expireResource: Loader dirty: {}", new Boolean(isDirty()));
+
+ // return the expiry status
+ return exp;
+ }
+
+ /**
+ * Returns the list of classpath entries after resetting each of them.
+ *
+ * @param list The list of {@link ClassPathEntry}s to reset
+ *
+ * @return The list of reset {@link ClassPathEntry}s.
+ */
+ private ClassPathEntry[] resetClassPathEntries(
+ ClassPathEntry[] oldClassPath) {
+ if (oldClassPath != null) {
+ for (int i=0; i < oldClassPath.length; i++) {
+ ClassPathEntry entry = oldClassPath[i];
+ log.debug("resetClassPathEntries: Cloning {}", entry);
+ oldClassPath[i] = entry.copy();
+ }
+ } else {
+ log.debug("resetClassPathEntries: No list to reset");
+ }
+ return oldClassPath;
+ }
+
+ protected final static class ProxyEventListener implements EventListener {
+
+ private final EventListener delegatee;
+
+ public ProxyEventListener(final EventListener delegatee) {
+ this.delegatee = delegatee;
+ }
+ /**
+ * @see javax.jcr.observation.EventListener#onEvent(javax.jcr.observation.EventIterator)
+ */
+ public void onEvent(EventIterator events) {
+ this.delegatee.onEvent(events);
+ }
+ }
+}
diff --git a/src/main/java/org/apache/sling/jcr/classloader/internal/RepositoryClassLoaderFacade.java b/src/main/java/org/apache/sling/jcr/classloader/internal/RepositoryClassLoaderFacade.java
index f33a834..d52ee8f 100644
--- a/src/main/java/org/apache/sling/jcr/classloader/internal/RepositoryClassLoaderFacade.java
+++ b/src/main/java/org/apache/sling/jcr/classloader/internal/RepositoryClassLoaderFacade.java
@@ -25,7 +25,6 @@ import java.util.Enumeration;
import javax.jcr.RepositoryException;
-import org.apache.jackrabbit.classloader.DynamicRepositoryClassLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
diff --git a/src/main/java/org/apache/sling/jcr/classloader/internal/URLRepositoryClassLoader.java b/src/main/java/org/apache/sling/jcr/classloader/internal/URLRepositoryClassLoader.java
new file mode 100644
index 0000000..7755fd9
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/classloader/internal/URLRepositoryClassLoader.java
@@ -0,0 +1,795 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * 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.sling.jcr.classloader.internal;
+
+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.sling.jcr.classloader.internal.net.JCRURLConnection;
+import org.apache.sling.jcr.classloader.internal.net.URLFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * 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>
+ */
+public class URLRepositoryClassLoader extends URLClassLoader {
+
+ /** default log category */
+ private final Logger log =
+ LoggerFactory.getLogger(this.getClass().getName());
+
+ /** 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 paths to use as a classpath.
+ */
+ private String[] paths;
+
+ /**
+ * 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<String, ClassLoaderResource> 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 URLRepositoryClassLoader(Session session, String[] classPath,
+ 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 (classPath == null || classPath.length == 0) {
+ throw new NullPointerException("handles");
+ }
+
+ // set fields
+ this.session = session;
+ setPaths(classPath);
+ this.cache = new HashMap<String, ClassLoaderResource>();
+ this.destroyed = false;
+
+ // build the class repositories list
+ buildRepository();
+
+ log.debug("RepositoryClassLoader: {} ready", this);
+ }
+
+ /**
+ * Returns <code>true</code> if this class loader has already been destroyed
+ * by calling {@link #destroy()}.
+ */
+ protected boolean isDestroyed() {
+ return destroyed;
+ }
+
+ protected String[] getPaths() {
+ return this.paths;
+ }
+
+ protected void setPaths(final String[] classPath) {
+ this.paths = classPath;
+ }
+
+ /**
+ * 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);
+ setPaths(null);
+ session = null;
+
+ // clear the cache of loaded resources and flush cached class
+ // introspections of the JavaBean framework
+ if (cache != null) {
+ final Iterator<ClassLoaderResource> ci = cache.values().iterator();
+ while ( ci.hasNext() ) {
+ final ClassLoaderResource res = ci.next();
+ if (res.getLoadedClass() != null) {
+ Introspector.flushFromCaches(res.getLoadedClass());
+ res.setLoadedClass(null);
+ }
+ }
+ cache.clear();
+ }
+ }
+
+ //---------- 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 {}, created {}",
+ res, 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 {}, created {}",
+ res, 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.
+ * @see java.net.URLClassLoader#getURLs()
+ */
+ 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.
+ * @see java.net.URLClassLoader#addURL(java.net.URL)
+ */
+ 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: {} is not a Repository URL, ignored", url);
+ }
+ }
+
+ //---------- Property access ----------------------------------------------
+
+ /**
+ * 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 {} not cached", name);
+ 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() {
+ final Iterator<ClassLoaderResource> ci = this.cache.values().iterator();
+ while (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() {
+ StringBuilder buf = new StringBuilder(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() {
+ List<ClassPathEntry> newRepository = new ArrayList<ClassPathEntry>(paths.length);
+
+ // build repository from path patterns
+ for (int i=0; i < paths.length; i++) {
+ final String entry = paths[i];
+ ClassPathEntry cp = null;
+
+ // try to find repository based on this path
+ if (repository != null) {
+ for (int j=0; j < repository.length; j++) {
+ final 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
+ final 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 {} for class {}",
+ path, name);
+
+ ClassLoaderResource res = findClassLoaderResource(path);
+ if (res != null) {
+
+ // try defining the class, error aborts
+ try {
+ log.debug(
+ "findClassPrivileged: Loading class from {}, created {}",
+ res, 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 = cache.get(name);
+ if (res == NOT_FOUND_RESOURCE) {
+ log.debug("Resource '{}' known to not exist in class path", name);
+ 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 {}, created ", res, 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());
+ }
+}
diff --git a/src/main/java/org/apache/sling/jcr/classloader/internal/Util.java b/src/main/java/org/apache/sling/jcr/classloader/internal/Util.java
new file mode 100644
index 0000000..01e41f8
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/classloader/internal/Util.java
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * 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.sling.jcr.classloader.internal;
+
+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.ValueFormatException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 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.
+ */
+public class Util {
+
+ /** default logging */
+ private static final Logger log = LoggerFactory.getLogger(Util.class);
+
+ /** Private constructor to not instantiate */
+ private Util() {
+ }
+
+ /**
+ * Resolves the given <code>item</code> to a <code>Property</code> from
+ * which contents can be read.
+ * <p>
+ * The following mechanism is used to derive the contents:
+ * <ol>
+ * <li>If the <code>item</code> is a property, this property is used</li>
+ * <li>If the <code>item</code> is a node, three steps are tested:
+ * <ol>
+ * <li>If the node has a <code>jcr:content</code> child node, use that
+ * child node in the next steps. Otherwise continue with the node.</li>
+ * <li>Check for a <code>jcr:data</code> property and use that property
+ * if existing.</li>
+ * <li>Otherwise call <code>getPrimaryItem</code> method repeatedly until
+ * a property is returned or until no more primary item is available.</li>
+ * </ol>
+ * </ol>
+ * If no property can be resolved using the above algorithm or 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 or the
+ * <code>item</code> is a node which cannot be resolved to a data
+ * property.
+ * @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 ValueFormatException,
+ RepositoryException {
+
+ Property prop;
+ if (item.isNode()) {
+
+ // check whether the node has a jcr:content node (e.g. nt:file)
+ Node node = (Node) item;
+ if (node.hasNode("jcr:content")) {
+ node = node.getNode("jcr:content");
+ }
+
+ // if the node has a jcr:data property, use that property
+ if (node.hasProperty("jcr:data")) {
+
+ prop = node.getProperty("jcr:data");
+
+ } else {
+
+ // otherwise try to follow default item trail
+ try {
+ item = node.getPrimaryItem();
+ while (item.isNode()) {
+ item = ((Node) item).getPrimaryItem();
+ }
+ prop = (Property) item;
+ } catch (ItemNotFoundException infe) {
+ // we don't actually care, but log for completeness
+ log.debug("getProperty: No primary items for "
+ + node.getPath(), infe);
+ return null;
+ }
+ }
+
+ } else {
+
+ prop = (Property) item;
+
+ }
+
+ // we get here with a property - otherwise an exception has already
+ // been thrown
+ if (prop.getDefinition().isMultiple()) {
+ log.error("{} is a multivalue property", prop.getPath());
+ return null;
+ } else if (prop.getType() == PropertyType.REFERENCE) {
+ Node node = prop.getNode();
+ log.info("Property {} refers to node {}; finding primary item",
+ prop.getPath(), node.getPath());
+ return getProperty(node);
+ }
+
+ return prop;
+ }
+
+ /**
+ * Returns the last modification time of the property, which is the long
+ * value of the <code>jcr:lastModified</code> property of the parent node
+ * of <code>prop</code>. If the parent node does not have a
+ * <code>jcr:lastModified</code> property 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 parent node of the property does not have a
+ * <code>jcr:lastModified</code> property.
+ * @throws ItemNotFoundException If the parent node of the property cannot
+ * be retrieved.
+ * @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.hasProperty("jcr:lastModified")) {
+ return parent.getProperty("jcr:lastModified").getLong();
+ }
+
+ return System.currentTimeMillis();
+ }
+}
diff --git a/src/main/java/org/apache/sling/jcr/classloader/internal/net/FileParts.java b/src/main/java/org/apache/sling/jcr/classloader/internal/net/FileParts.java
new file mode 100644
index 0000000..a5057f3
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/classloader/internal/net/FileParts.java
@@ -0,0 +1,312 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * 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.sling.jcr.classloader.internal.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
+ */
+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() {
+ StringBuilder buf = new StringBuilder();
+ 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 + "'");
+ }
+}
diff --git a/src/main/java/org/apache/sling/jcr/classloader/internal/net/JCRJarURLConnection.java b/src/main/java/org/apache/sling/jcr/classloader/internal/net/JCRJarURLConnection.java
new file mode 100644
index 0000000..29d4a20
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/classloader/internal/net/JCRJarURLConnection.java
@@ -0,0 +1,289 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * 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.sling.jcr.classloader.internal.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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The <code>JCRJarURLConnection</code> extends the
+ * {@link org.apache.sling.jcr.classloader.internal.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
+ */
+public class JCRJarURLConnection extends JCRURLConnection {
+
+ /** default log category */
+ private static final Logger log =
+ LoggerFactory.getLogger(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;
+ }
+}
diff --git a/src/main/java/org/apache/sling/jcr/classloader/internal/net/JCRJarURLHandler.java b/src/main/java/org/apache/sling/jcr/classloader/internal/net/JCRJarURLHandler.java
new file mode 100644
index 0000000..39b7f26
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/classloader/internal/net/JCRJarURLHandler.java
@@ -0,0 +1,298 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * 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.sling.jcr.classloader.internal.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.sling.jcr.classloader.internal.net.URLFactory} class.
+ * <p>
+ * This class is not intended to be subclassed or instantiated by clients.
+ *
+ * @author Felix Meschberger
+ *
+ * @see org.apache.sling.jcr.classloader.internal.net.JCRJarURLConnection
+ * @see org.apache.sling.jcr.classloader.internal.net.URLFactory
+ * @see org.apache.sling.jcr.classloader.internal.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;
+ }
+}
diff --git a/src/main/java/org/apache/sling/jcr/classloader/internal/net/JCRURLConnection.java b/src/main/java/org/apache/sling/jcr/classloader/internal/net/JCRURLConnection.java
new file mode 100644
index 0000000..9e1471a
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/classloader/internal/net/JCRURLConnection.java
@@ -0,0 +1,778 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * 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.sling.jcr.classloader.internal.net;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.jcr.Item;
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.apache.sling.jcr.classloader.internal.Util;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * The <code>JCRURLConnection</code> is the <code>URLConnection</code>
+ * implementation to access the data addressed by a JCR Repository URL.
+ * <p>
+ * As the primary use of a <code>URLConnection</code> and thus the
+ * <code>JCRURLConnection</code> is to provide access to the content of a
+ * resource identified by the URL, it is the primary task of this class to
+ * identify and access a repository <code>Property</code> based on the URL. This
+ * main task is executed in the {@link #connect()} method.
+ * <p>
+ * Basically the guideposts to access content from a JCR Repository URL are
+ * the following:
+ * <ul>
+ * <li>The URL must ultimately resolve to a repository property to provide
+ * content.
+ * <li>If the URL itself is the path to a property, that property is used to
+ * provide the content.
+ * <li>If the URL is a path to a node, either the
+ * <code>jcr:content/jcr:data</code> or <code>jcr:data</code> property is
+ * used or the primary item chain starting with this node is followed until
+ * no further primary items exist. If the final item is a property, that
+ * property is used to provide the content.
+ * <li>If neither of the above methods resolve to a property, the
+ * {@link #connect()} fails and access to the content is not possible.
+ * </ul>
+ * <p>
+ * After having connected the property is available through the
+ * {@link #getProperty()} method. Other methods exist to retrieve repository
+ * related information defined when creating the URL: {@link #getSession()} to
+ * retrieve the session of the URL, {@link #getPath()} to retrieve the path
+ * with which the URL was created and {@link #getItem()} to retrieve the item
+ * with which the URL was created. The results of calling {@link #getProperty()}
+ * and {@link #getItem()} will be the same if the URL directly addressed the
+ * property. If the URL addressed the node whose primary item chain ultimately
+ * resolved to the property, the {@link #getItem()} will return the node and
+ * {@link #getProperty()} will return the resolved property.
+ * <p>
+ * A note on the <code>InputStream</code> available from
+ * {@link #getInputStream()}: Unlike other implementations - for example
+ * for <code>file:</code> or <code>http:</code> URLs - which return the same
+ * stream on each call, this implementation returns a new stream on each
+ * invocation.
+ * <p>
+ * The following header fields are implemented by this class:
+ * <dl>
+ * <dt><code>Content-Length</code>
+ * <dd>The size of the content is filled from the <code>Property.getLength()</code>
+ * method, which returns the size in bytes of the property's value for
+ * binary values and the number of characters used for the string
+ * representation of the value for all other value types.
+ *
+ * <dt><code>Content-Type</code>
+ * <dd>The content type is retrieved from the <code>jcr:mimeType</code>
+ * property of the property's parent node if existing. Otherwise the
+ * <code>guessContentTypeFromName</code> method is called on the
+ * {@link #getPath() path}. If this does not yield a content type, it is
+ * set to <code>application/octet-stream</code> for binary properties and
+ * to <code>text/plain</code> for other types.
+ *
+ * <dt><code>Content-Enconding</code>
+ * <dd>The content encoding is retrieved from the <code>jcr:econding</code>
+ * property of the property's parent node if existing. Otherwise this
+ * header field remains undefined (aka <code>null</code>).
+ *
+ * <dt><code>Last-Modified</code>
+ * <dd>The last modified type is retrieved from the <code>jcr:lastModified</code>
+ * property of the property's parent node if existing. Otherwise the last
+ * modification time is set to zero.
+ * </dl>
+ * <p>
+ * This class is not intended to be subclassed or instantiated by clients.
+ */
+public class JCRURLConnection extends URLConnection {
+
+ /** Default logging */
+ private static final Logger log =
+ LoggerFactory.getLogger(JCRURLConnection.class);
+
+ /**
+ * The name of the header containing the content size (value is
+ * "content-length").
+ */
+ protected static final String CONTENT_LENGTH = "content-length";
+
+ /**
+ * The name of the header containing the MIME type of the content (value is
+ * "content-type").
+ */
+ protected static final String CONTENT_TYPE = "content-type";
+
+ /**
+ * The name of the header containing the content encoding (value is
+ * "content-encoding").
+ */
+ protected static final String CONTENT_ENCODING = "content-encoding";
+
+ /**
+ * The name of the header containing the last modification time stamp of
+ * the content (value is "last-modified").
+ */
+ protected static final String LAST_MODIFIED = "last-modified";
+
+ /**
+ * The default content type name for binary properties accessed by this
+ * connection (value is "application/octet-stream").
+ * @see #connect()
+ */
+ protected static final String APPLICATION_OCTET = "application/octet-stream";
+
+ /**
+ * The default content type name for non-binary properties accessed by this
+ * connection (value is "text/plain").
+ * @see #connect()
+ */
+ protected static final String TEXT_PLAIN = "text/plain";
+
+ /**
+ * The handler associated with the URL of this connection. This handler
+ * provides the connection with access to the repository and the item
+ * underlying the URL.
+ */
+ private final JCRURLHandler handler;
+
+ /**
+ * The {@link FileParts} encapsulating the repository name, workspace name,
+ * item path and optional archive entry path contained in the file part
+ * of the URL. This field is set on-demand by the {@link #getFileParts()}
+ * method.
+ *
+ * @see #getFileParts()
+ */
+ private FileParts fileParts;
+
+ /**
+ * The <code>Item</code> addressed by the path of this connection's URL.
+ * This field is set on-demand by the {@link #getItem()} method.
+ *
+ * @see #getItem()
+ */
+ private Item item;
+
+ /**
+ * The <code>Property</code> associated with the URLConnection. The field
+ * is only set after the connection has been successfully opened.
+ *
+ * @see #getProperty()
+ * @see #connect()
+ */
+ private Property property;
+
+ /**
+ * The (guessed) content type of the data. Currently the content type is
+ * guessed based on the path name of the page or the binary attribute of the
+ * atom.
+ * <p>
+ * Implementations are free to decide, how to define the content type. But
+ * they are required to set the type in the {@link #connect(Ticket)}method.
+ *
+ * @see #getContentType()
+ * @see #connect()
+ */
+ private String contentType;
+
+ /**
+ * The (guessed) content encoding of the data. Currently the content type is
+ * guessed based on the path name of the page or the binary attribute of the
+ * atom.
+ * <p>
+ * Implementations are free to decide, how to define the content type. But
+ * they are required to set the type in the {@link #connect(Ticket)}method.
+ *
+ * @see #getContentEncoding()
+ * @see #connect()
+ */
+ private String contentEncoding;
+
+ /**
+ * The content lentgh of the data, which is the size field of the atom
+ * status information of the base atom.
+ * <p>
+ * Implementations are free to decide, how to define the content length. But
+ * they are required to set the type in the {@link #connect(Ticket)}method.
+ *
+ * @see #getContentLength()
+ * @see #connect()
+ */
+ private int contentLength;
+
+ /**
+ * The last modification time in milliseconds since the epoch (1970/01/01)
+ * <p>
+ * Implementations are free to decide, how to define the last modification
+ * time. But they are required to set the type in the
+ * {@link #connect(Ticket)}method.
+ *
+ * @see #getLastModified()
+ * @see #connect()
+ */
+ private long lastModified;
+
+ /**
+ * 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.
+ */
+ JCRURLConnection(URL url, JCRURLHandler handler) {
+ super(url);
+ this.handler = handler;
+ }
+
+ /**
+ * Returns the current session of URL.
+ * <p>
+ * Calling this method does not require this connection being connected.
+ */
+ public Session getSession() {
+ return handler.getSession();
+ }
+
+ /**
+ * Returns the path to the repository item underlying the URL of this
+ * connection.
+ * <p>
+ * Calling this method does not require this connection being connected.
+ */
+ public String getPath() {
+ return getFileParts().getPath();
+ }
+
+ /**
+ * Returns the repository item underlying the URL of this connection
+ * retrieved through the path set on the URL.
+ * <p>
+ * Calling this method does not require this connection being connected.
+ *
+ * @throws IOException If the item has to be retrieved from the repository
+ * <code>Session</code> of this connection and an error occurrs. The
+ * cause of the exception will refer to the exception thrown from the
+ * repository. If the path addresses a non-existing item, the cause
+ * will be a <code>PathNotFoundException</code>.
+ */
+ public Item getItem() throws IOException {
+ if (item == null) {
+ try {
+ item = getSession().getItem(getPath());
+ } catch (RepositoryException re) {
+ throw failure("getItem", re.toString(), re);
+ }
+ }
+
+ return item;
+ }
+
+ /**
+ * Returns the repository <code>Property</code> providing the contents of
+ * this connection.
+ * <p>
+ * Calling this method forces the connection to be opened by calling the
+ * {@link #connect()} method.
+ *
+ * @throws IOException May be thrown by the {@link #connect()} method called
+ * by this method.
+ *
+ * @see #connect()
+ */
+ public Property getProperty() throws IOException {
+ // connect to set the property value
+ connect();
+
+ return property;
+ }
+
+ //---------- URLConnection overwrites -------------------------------------
+
+ /**
+ * Connects to the URL setting the header fields and preparing for the
+ * {@link #getProperty()} and {@link #getInputStream()} methods.
+ * <p>
+ * The following algorithm is applied:
+ * <ol>
+ * <li>The repository item is retrieved from the URL's
+ * <code>URLHandler</code>.
+ * <li>If the item is a node, the <code>getPrimaryItem</code> method is
+ * called on that node. If the node has no primary item, the connection
+ * fails.
+ * <li>If the item - either from the handler or after calling
+ * <code>getPrimaryItem</code> is still a node, this method fails
+ * because a <code>Property</code> is required for a successfull
+ * connection.
+ * <li>If the property found above is a multi-valued property, connection
+ * fails, because multi-valued properties are not currently supported.
+ * <li>The content length header field is set from the property length
+ * (<code>Property.getLength())</code>).
+ * <li>The header fields for the content type, content encoding and last
+ * modification time are set from the <code>jcr:mimeType</code>,
+ * <code>jcr:encoding</code>, and <code>jcr:lastModification</code>
+ * properties of the property's parent node if existing. Otherwise the
+ * content encoding field is set to <code>null</code> and the last
+ * modification time is set to zero. The content type field is guessed
+ * from the name of the URL item. If the content type cannot be
+ * guessed, it is set to <code>application/octet-stream</code> if the
+ * property is of binary type or <code>text/plain</code> otherwise.
+ * </ol>
+ * <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 {
+ // todo: The ContentBus URL must also contain version information on
+ if (!connected) {
+
+ // Get hold of the data
+ try {
+ // resolve the URLs item to a property
+ Property property = Util.getProperty(getItem());
+ if (property == null) {
+ throw failure("connect",
+ "Multivalue property not supported", null);
+ }
+
+ // values to set later
+ String contentType;
+ String contentEncoding = null; // no defined content encoding
+ int contentLength = (int) property.getLength();
+ long lastModified;
+
+ Node parent = property.getParent();
+ if (parent.hasProperty("jcr:lastModified")) {
+ lastModified = parent.getProperty("jcr:lastModified").getLong();
+ } else {
+ lastModified = 0;
+ }
+
+ if (parent.hasProperty("jcr:mimeType")) {
+ contentType = parent.getProperty("jcr:mimeType").getString();
+ } else {
+ contentType = guessContentTypeFromName(getItem().getName());
+ if (contentType == null) {
+ contentType = (property.getType() == PropertyType.BINARY)
+ ? APPLICATION_OCTET
+ : TEXT_PLAIN;
+ }
+ }
+
+ if (parent.hasProperty("jcr:encoding")) {
+ contentEncoding = parent.getProperty("jcr:encoding").getString();
+ } else {
+ contentEncoding = null;
+ }
+
+ log.debug(
+ "connect: Using property '{}' with content type '{}' for {} bytes",
+ new Object[] { property.getPath(), contentType,
+ new Integer(contentLength) });
+
+ // set the fields
+ setProperty(property);
+ 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.
+ * <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.
+ *
+ * @throws IOException if an error occurrs opening the connection through
+ * {@link #connect()} or creating the <code>InputStream</code> on the
+ * repository <code>Property</code>.
+ *
+ * @see #connect()
+ */
+ public InputStream getInputStream() throws IOException {
+ try {
+ return getProperty().getStream();
+ } catch (RepositoryException re) {
+ throw failure("getInputStream", re.toString(), re);
+ }
+ }
+
+ /**
+ * Gets the named header field. This implementation only supports the
+ * Content-Type, Content-Encoding, Content-Length and Last-Modified header
+ * fields. All other names return <code>null</code>.
+ * <p>
+ * Calling this method implicitly calls {@link #connect()} to ensure the
+ * connection is open.
+ *
+ * @param s The name of the header field value to return.
+ *
+ * @return The corresponding value or <code>null</code> if not one of the
+ * supported fields or the named field's value cannot be retrieved
+ * from the data source.
+ *
+ * @see #connect()
+ */
+ public String getHeaderField(String s) {
+ try {
+ connect();
+ if (CONTENT_LENGTH.equalsIgnoreCase(s)) {
+ return String.valueOf(contentLength);
+ } else if (CONTENT_TYPE.equalsIgnoreCase(s)) {
+ return contentType;
+ } else if (LAST_MODIFIED.equalsIgnoreCase(s)) {
+ return String.valueOf(lastModified);
+ } else if (CONTENT_ENCODING.equalsIgnoreCase(s)) {
+ return contentEncoding;
+ }
+ } catch (IOException ioe) {
+ log.info("getHeaderField: Problem connecting: " + ioe.toString());
+ log.debug("dump", ioe);
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the header field with the given index. As with
+ * {@link #getHeaderField(String)} only Content-Length, Content-Type,
+ * Content-Encoding, and Last-Modified are supported. All indexes other
+ * than 0, 1, 2 or 3 will return <code>null</code>.
+ * <p>
+ * Calling this method implicitly calls {@link #connect()} to ensure the
+ * connection is open.
+ *
+ * @param i The index of the header field value to return.
+ *
+ * @return The corresponding value or <code>null</code> if not one of the
+ * supported fields or the known field's value cannot be retrieved
+ * from the data source.
+ *
+ * @see #connect()
+ */
+ public String getHeaderField(int i) {
+ try {
+ connect();
+ if (i == 0) {
+ return String.valueOf(contentLength);
+ } else if (i == 1) {
+ return contentType;
+ } else if (i == 2) {
+ return String.valueOf(lastModified);
+ } else if (i == 3) {
+ return contentEncoding;
+ }
+ } catch (IOException ioe) {
+ log.info("getHeaderField: Problem connecting: " + ioe.toString());
+ log.debug("dump", ioe);
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the name of the header field with the given index. As with
+ * {@link #getHeaderField(String)} only Content-Length, Content-Type,
+ * Content-Encoding and Last-Modified are supported. All indexes other than
+ * 0, 1, 2 or 3 will return <code>null</code>.
+ * <p>
+ * Calling this method implicitly calls {@link #connect()} to ensure the
+ * connection is open.
+ *
+ * @param i The index of the header field name to return.
+ * @return The corresponding name or <code>null</code> if not one of the
+ * supported fields.
+ *
+ * @see #connect()
+ */
+ public String getHeaderFieldKey(int i) {
+ try {
+ connect();
+ if (i == 0) {
+ return CONTENT_LENGTH;
+ } else if (i == 1) {
+ return CONTENT_TYPE;
+ } else if (i == 2) {
+ return LAST_MODIFIED;
+ } else if (i == 3) {
+ return CONTENT_ENCODING;
+ }
+ } catch (IOException ioe) {
+ log
+ .info("getHeaderFieldKey: Problem connecting: "
+ + ioe.toString());
+ log.debug("dump", ioe);
+ }
+ return null;
+ }
+
+ /**
+ * Returns an unmodifiable map of all header fields. Each entry is indexed
+ * with a string key naming the field. The entry's value is an unmodifiable
+ * list of the string values of the respective header field.
+ * <p>
+ * Calling this method implicitly calls {@link #connect()} to ensure the
+ * connection is open.
+ *
+ * @return An unmodifiable map of header fields and their values. The map
+ * will be empty if an error occurrs connecting through
+ * {@link #connect()}.
+ *
+ * @see #connect()
+ */
+ public Map getHeaderFields() {
+ Map fieldMap = new HashMap();
+
+ try {
+ connect();
+ fieldMap.put(CONTENT_LENGTH, toList(String.valueOf(contentLength)));
+ fieldMap.put(CONTENT_TYPE, toList(contentType));
+ fieldMap.put(LAST_MODIFIED, toList(String.valueOf(lastModified)));
+
+ // only include if not null))
+ if (contentEncoding != null) {
+ fieldMap.put(CONTENT_ENCODING, toList(contentEncoding));
+ }
+ } catch (IOException ioe) {
+ log.info("getHeaderFields: Problem connecting: " + ioe.toString());
+ log.debug("dump", ioe);
+ }
+
+ return Collections.unmodifiableMap(fieldMap);
+ }
+
+ /**
+ * Returns the content type of the data as a string. This is just a
+ * perfomance convenience overwrite of the base class implementation.
+ * <p>
+ * Calling this method implicitly calls {@link #connect()} to ensure the
+ * connection is open.
+ *
+ * @return The content length of the data or <code>null</code> if the
+ * content type cannot be derived from the data source.
+ *
+ * @see #connect()
+ */
+ public String getContentType() {
+ try {
+ connect();
+ return contentType;
+ } catch (IOException ioe) {
+ log.info("getContentType: Problem connecting: " + ioe.toString());
+ log.debug("dump", ioe);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the content encoding of the data as a string. This is just a
+ * perfomance convenience overwrite of the base class implementation.
+ * <p>
+ * Calling this method implicitly calls {@link #connect()} to ensure the
+ * connection is open.
+ *
+ * @return The content encoding of the data or <code>null</code> if the
+ * content encoding cannot be derived from the data source.
+ *
+ * @see #connect()
+ */
+ public String getContentEncoding() {
+ try {
+ connect();
+ return contentEncoding;
+ } catch (IOException ioe) {
+ log.info("getContentEncoding: Problem connecting: " + ioe.toString());
+ log.debug("dump", ioe);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the content length of the data as an number. This is just a
+ * perfomance convenience overwrite of the base class implementation.
+ * <p>
+ * Calling this method implicitly calls {@link #connect()} to ensure the
+ * connection is open.
+ *
+ * @return The content length of the data or -1 if the content length cannot
+ * be derived from the data source.
+ *
+ * @see #connect()
+ */
+ public int getContentLength() {
+ try {
+ connect();
+ return contentLength;
+ } catch (IOException ioe) {
+ log.info("getContentLength: Problem connecting: " + ioe.toString());
+ log.debug("dump", ioe);
+ }
+ return -1;
+ }
+
+ /**
+ * Returns the value of the <code>last-modified</code> header field. The
+ * result is the number of milliseconds since January 1, 1970 GMT.
+ * <p>
+ * Calling this method implicitly calls {@link #connect()} to ensure the
+ * connection is open.
+ *
+ * @return the date the resource referenced by this
+ * <code>URLConnection</code> was last modified, or -1 if not
+ * known.
+ *
+ * @see #connect()
+ */
+ public long getLastModified() {
+ try {
+ connect();
+ return lastModified;
+ } catch (IOException ioe) {
+ log.info("getLastModified: Problem connecting: " + ioe.toString());
+ log.debug("dump", ioe);
+ }
+ return -1;
+ }
+
+ //---------- implementation helpers ----------------------------------------
+
+ /**
+ * Returns the URL handler of the URL of this connection.
+ */
+ protected JCRURLHandler getHandler() {
+ return handler;
+ }
+
+ /**
+ * Returns the {@link FileParts} object which contains the decomposed file
+ * part of this connection's URL.
+ */
+ FileParts getFileParts() {
+ if (fileParts == null) {
+ fileParts = new FileParts(getURL().getFile());
+ }
+
+ return fileParts;
+ }
+
+ /**
+ * @param contentEncoding The contentEncoding to set.
+ */
+ protected void setContentEncoding(String contentEncoding) {
+ this.contentEncoding = contentEncoding;
+ }
+
+ /**
+ * @param contentLength The contentLength to set.
+ */
+ protected void setContentLength(int contentLength) {
+ this.contentLength = contentLength;
+ }
+
+ /**
+ * @param contentType The contentType to set.
+ */
+ protected void setContentType(String contentType) {
+ this.contentType = contentType;
+ }
+
+ /**
+ * @param lastModified The lastModified to set.
+ */
+ protected void setLastModified(long lastModified) {
+ this.lastModified = lastModified;
+ }
+
+ /**
+ * @param property The property to set.
+ */
+ protected void setProperty(Property property) {
+ this.property = property;
+ }
+
+ //---------- internal -----------------------------------------------------
+
+ /**
+ * Logs the message and returns an IOException to be thrown by the caller.
+ * The log message contains the caller name, the external URL form and the
+ * message while the IOException is only based on the external URL form and
+ * the message given.
+ *
+ * @param method The method in which the error occurred. This is used for
+ * logging.
+ * @param message The message to log and set in the exception
+ * @param cause The cause of failure. May be <code>null</code>.
+ *
+ * @return The IOException the caller may throw.
+ */
+ protected IOException failure(String method, String message, Throwable cause) {
+ log.info(method + ": URL: " + url.toExternalForm() + ", Reason: "
+ + message);
+
+ if (cause != null) {
+ log.debug("dump", cause);
+ }
+
+ IOException ioe = new IOException(url.toExternalForm() + ": " + message);
+ ioe.initCause(cause);
+ return ioe;
+ }
+
+ /**
+ * Returns an unmodifiable list containing just the given string value.
+ */
+ private List toList(String value) {
+ String[] values = { value };
+ List valueList = Arrays.asList(values);
+ return Collections.unmodifiableList(valueList);
+ }
+}
diff --git a/src/main/java/org/apache/sling/jcr/classloader/internal/net/JCRURLHandler.java b/src/main/java/org/apache/sling/jcr/classloader/internal/net/JCRURLHandler.java
new file mode 100644
index 0000000..1fa72c6
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/classloader/internal/net/JCRURLHandler.java
@@ -0,0 +1,147 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * 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.sling.jcr.classloader.internal.net;
+
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLStreamHandler;
+
+import javax.jcr.Session;
+
+/**
+ * The <code>JCRURLHandler</code> is the <code>URLStreamHandler</code> for
+ * JCR Repository URLs identified by the scheme <code>jcr</code>.
+ * <p>
+ * JCR Repository 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.sling.jcr.classloader.internal.net.URLFactory}
+ * class.
+ * <p>
+ * This class is not intended to be subclassed or instantiated by clients.
+ *
+ * @author Felix Meschberger
+ *
+ * @see org.apache.sling.jcr.classloader.internal.net.JCRURLConnection
+ * @see org.apache.sling.jcr.classloader.internal.net.URLFactory
+ * @see org.apache.sling.jcr.classloader.internal.net.URLFactory#createURL(Session, String)
+ */
+class JCRURLHandler extends URLStreamHandler {
+
+ /**
+ * The session used to create this handler, which is also used to open
+ * the connection object.
+ *
+ * @see #getSession()
+ */
+ private final Session session;
+
+ /**
+ * Creates a new instance of the <code>JCRURLHandler</code> with the
+ * given session.
+ *
+ * @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>.
+ */
+ JCRURLHandler(Session session) {
+ if (session == null) {
+ throw new NullPointerException("session");
+ }
+
+ this.session = session;
+ }
+
+ /**
+ * Returns the session supporting this handler.
+ */
+ Session getSession() {
+ return session;
+ }
+
+ //---------- URLStreamHandler abstracts ------------------------------------
+
+ /**
+ * Gets a connection object to connect to an JCR Repository URL.
+ *
+ * @param url The JCR Repository URL to connect to.
+ *
+ * @return An instance of the {@link JCRURLConnection} class.
+ *
+ * @see JCRURLConnection
+ */
+ protected URLConnection openConnection(URL url) {
+ return new JCRURLConnection(url, this);
+ }
+
+ /**
+ * Checks the new <code>authority</code> and <code>path</code> before
+ * actually setting the values on the url calling the base class
+ * implementation.
+ * <p>
+ * We check the authority to not have been modified from the original URL,
+ * as the authority is dependent on the repository <code>Session</code> on
+ * which this handler is based and which was used to create the original
+ * URL. Likewise the repository and workspace name parts of the path must
+ * not have changed.
+ *
+ * @param u the URL to modify.
+ * @param protocol the protocol name.
+ * @param host the remote host value for the URL.
+ * @param port the port on the remote machine.
+ * @param authority the authority part for the URL.
+ * @param userInfo the userInfo part of the URL.
+ * @param path the path component of the URL.
+ * @param query the query part for the URL.
+ * @param ref the reference.
+ *
+ * @throws IllegalArgumentException if the authority or the repository name
+ * or workspace name parts of the path has changed.
+ */
+ protected void setURL(URL u, String protocol, String host, int port,
+ String authority, String userInfo, String path, String query, String ref) {
+
+ // check for authority
+ if (u.getAuthority() != authority) {
+ if (u.getAuthority() == null) {
+ if (authority != null) {
+ throw new IllegalArgumentException("Authority " +
+ authority + " not supported by this handler");
+ }
+ } else if (!u.getAuthority().equals(authority)) {
+ throw new IllegalArgumentException("Authority " +
+ authority + " not supported by this handler");
+ }
+ }
+
+ // check for repository and/or workspace modifications
+ FileParts newParts = new FileParts(path);
+ if (!"_".equals(newParts.getRepository())) {
+ throw new IllegalArgumentException("Repository " +
+ newParts.getRepository() + " not supported by this handler");
+ }
+ if (!session.getWorkspace().getName().equals(newParts.getWorkspace())) {
+ throw new IllegalArgumentException("Workspace " +
+ newParts.getWorkspace() + " not supported by this handler");
+ }
+
+ // finally set the new values on the URL
+ super.setURL(u, protocol, host, port, authority, userInfo, path, query,
+ ref);
+ }
+}
diff --git a/src/main/java/org/apache/sling/jcr/classloader/internal/net/URLFactory.java b/src/main/java/org/apache/sling/jcr/classloader/internal/net/URLFactory.java
new file mode 100644
index 0000000..834480a
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/classloader/internal/net/URLFactory.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * 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.sling.jcr.classloader.internal.net;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import javax.jcr.Session;
+
+/**
+ * The <code>URLFactory</code> class provides factory methods for creating
+ * JCR Repository and JCRJar URLs.
+ * <p>
+ * This class is not intended to be subclassed or instantiated by clients.
+ *
+ * @author Felix Meschberger
+ */
+public final class URLFactory {
+
+ /**
+ * The scheme for JCR Repository URLs (value is "jcr").
+ */
+ public static final String REPOSITORY_SCHEME = "jcr";
+
+ /**
+ * The scheme for JCRJar URLs (value is "jar").
+ */
+ public static final String REPOSITORY_JAR_SCHEME = "jar";
+
+ /** Private default constructor, not to be instantiated */
+ private URLFactory() {
+ }
+
+ /**
+ * Creates a new JCR Repository URL for the given session and item path.
+ *
+ * @param session The repository session providing access to the item.
+ * @param path The absolute path to the item. This must be an absolute
+ * path with a leading slash character. If this is <code>null</code>
+ * the root node path - <code>/</code> - is assumed.
+ *
+ * @return The JCR Repository URL
+ *
+ * @throws MalformedURLException If an error occurrs creating the
+ * <code>URL</code> instance.
+ */
+ public static URL createURL(Session session, String path)
+ throws MalformedURLException {
+
+ return new URL(REPOSITORY_SCHEME, "", -1,
+ new FileParts(session, path, null).toString(),
+ new JCRURLHandler(session));
+ }
+
+ /**
+ * Creates a new JCRJar URL for the given session, archive and entry.
+ *
+ * @param session The repository session providing access to the archive.
+ * @param path The absolute path to the archive. This must either be the
+ * property containing the archive or an item which resolves to such
+ * a property through its primary item chain. This must be an absolute
+ * path with a leading slash character. If this is <code>null</code>
+ * the root node path - <code>/</code> - is assumed.
+ * @param entry The entry within the archive. If <code>null</code>, the URL
+ * provides access to the archive itself.
+ *
+ * @return The JCRJar URL
+ *
+ * @throws MalformedURLException If an error occurrs creating the
+ * <code>URL</code> instance.
+ */
+ public static URL createJarURL(Session session, String path, String entry)
+ throws MalformedURLException {
+
+ JCRJarURLHandler handler = new JCRJarURLHandler(session);
+ String file = createURL(session, path).toExternalForm();
+
+ // append entry spec if not null
+ if (entry != null) {
+ file += "!/" + entry;
+ }
+
+ return new URL(REPOSITORY_JAR_SCHEME, "", -1, file, handler);
+ }
+}
--
To stop receiving notification emails like this one, please contact
"commits@sling.apache.org" <co...@sling.apache.org>.