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:59:32 UTC

[sling-org-apache-sling-resourceresolver] 02/47: SLING-2396 : Add new resourceresolver project

This is an automated email from the ASF dual-hosted git repository.

rombert pushed a commit to annotated tag org.apache.sling.resourceresolver-1.0.0
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-resourceresolver.git

commit 6163957bd76944dd0690fbf7bd97f90e04c1bc51
Author: Carsten Ziegeler <cz...@apache.org>
AuthorDate: Thu Jun 14 09:44:20 2012 +0000

    SLING-2396 : Add new resourceresolver project
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/bundles/resourceresolver@1350168 13f79535-47bb-0310-9956-ffa450edef68
---
 README.txt                                         |   27 +
 .../impl/ResourceResolverFactoryImpl.java          |  758 ++++
 .../impl/ResourceResolverImpl.java                 | 1365 +++++++
 .../console/ResourceResolverWebConsolePlugin.java  |  385 ++
 .../impl/helper/RedirectResource.java              |   72 +
 .../impl/helper/ResourceDecoratorTracker.java      |  140 +
 .../impl/helper/ResourceIterator.java              |  294 ++
 .../impl/helper/ResourceIteratorDecorator.java     |   55 +
 .../impl/helper/ResourcePathIterator.java          |   99 +
 .../resourceresolver/impl/helper/StarResource.java |  109 +
 .../sling/resourceresolver/impl/helper/URI.java    | 4200 ++++++++++++++++++++
 .../resourceresolver/impl/helper/URIException.java |  124 +
 .../resourceresolver/impl/mapping/MapEntries.java  |  782 ++++
 .../resourceresolver/impl/mapping/MapEntry.java    |  331 ++
 .../resourceresolver/impl/mapping/Mapping.java     |  207 +
 .../impl/tree/ProviderHandler.java                 |  107 +
 .../impl/tree/ResourceProviderEntry.java           |  464 +++
 .../impl/tree/RootResourceProviderEntry.java       |  141 +
 18 files changed, 9660 insertions(+)

diff --git a/README.txt b/README.txt
new file mode 100644
index 0000000..0055fc1
--- /dev/null
+++ b/README.txt
@@ -0,0 +1,27 @@
+Apache Sling Resource Resolver
+
+This bundle provides the Resource Resolver and Resource Resolver Factory
+
+Getting Started
+===============
+
+This component uses a Maven 3 (http://maven.apache.org/) build
+environment. It requires a Java 5 JDK (or higher) and Maven (http://maven.apache.org/)
+3.0.3 or later. We recommend to use the latest Maven version.
+
+If you have Maven 3 installed, you can compile and
+package the jar using the following command:
+
+    mvn package
+
+See the Maven 3 documentation for other build features.
+
+The latest source code for this component is available in the
+Subversion (http://subversion.apache.org/) source repository of
+the Apache Software Foundation. If you have Subversion installed,
+you can checkout the latest source using the following command:
+
+    svn checkout http://svn.apache.org/repos/asf/sling/trunk/resourceresolver
+
+See the Subversion documentation for other source control features.
+
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverFactoryImpl.java b/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverFactoryImpl.java
new file mode 100644
index 0000000..969f280
--- /dev/null
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverFactoryImpl.java
@@ -0,0 +1,758 @@
+/*
+ * 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.resource.internal;
+
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+import javax.jcr.Credentials;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.SimpleCredentials;
+
+import org.apache.commons.collections.BidiMap;
+import org.apache.commons.collections.bidimap.TreeBidiMap;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Properties;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.PropertyUnbounded;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.ReferencePolicy;
+import org.apache.felix.scr.annotations.References;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.ResourceDecorator;
+import org.apache.sling.api.resource.ResourceProvider;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.commons.classloader.DynamicClassLoaderManager;
+import org.apache.sling.commons.osgi.OsgiUtil;
+import org.apache.sling.jcr.api.SlingRepository;
+import org.apache.sling.jcr.resource.JcrResourceConstants;
+import org.apache.sling.jcr.resource.JcrResourceResolverFactory;
+import org.apache.sling.jcr.resource.internal.helper.MapEntries;
+import org.apache.sling.jcr.resource.internal.helper.Mapping;
+import org.apache.sling.jcr.resource.internal.helper.ResourceProviderEntry;
+import org.apache.sling.jcr.resource.internal.helper.RootResourceProviderEntry;
+import org.apache.sling.jcr.resource.internal.helper.jcr.JcrResourceProviderEntry;
+import org.osgi.framework.Constants;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.event.EventAdmin;
+import org.osgi.util.tracker.ServiceTracker;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The <code>JcrResourceResolverFactoryImpl</code> is the
+ * {@link JcrResourceResolverFactory} service providing the following
+ * functionality:
+ * <ul>
+ * <li><code>JcrResourceResolverFactory</code> service
+ * <li>Bundle listener to load initial content and manage OCM mapping
+ * descriptors provided by bundles.
+ * <li>Fires OSGi EventAdmin events on behalf of internal helper objects
+ * </ul>
+ *
+ * First attempt of an resource resolver factory implementation.
+ * WORK IN PROGRESS - see SLING-1262
+ */
+@Component(immediate=true, label="%resource.resolver.name", description="%resource.resolver.description", specVersion="1.1", metatype=true)
+@Service(value={JcrResourceResolverFactory.class, ResourceResolverFactory.class})
+@Properties({
+    @Property(name = Constants.SERVICE_DESCRIPTION, value="Sling JcrResourceResolverFactory Implementation"),
+    @Property(name = Constants.SERVICE_VENDOR, value="The Apache Software Foundation")
+    
+})
+@References({
+    @Reference(name="ResourceProvider", referenceInterface=ResourceProvider.class, cardinality=ReferenceCardinality.OPTIONAL_MULTIPLE, policy=ReferencePolicy.DYNAMIC),
+    @Reference(name="ResourceDecorator", referenceInterface=ResourceDecorator.class, cardinality=ReferenceCardinality.OPTIONAL_MULTIPLE, policy=ReferencePolicy.DYNAMIC)    
+})
+public class JcrResourceResolverFactoryImpl implements
+        JcrResourceResolverFactory, ResourceResolverFactory {
+
+    public final static class ResourcePattern {
+        public final Pattern pattern;
+
+        public final String replacement;
+
+        public ResourcePattern(final Pattern p, final String r) {
+            this.pattern = p;
+            this.replacement = r;
+        }
+    }
+
+    private static final boolean DEFAULT_MULTIWORKSPACE = false;
+
+    /**
+     * Special value which, if passed to listener.workspaces, will have resource
+     * events fired for all workspaces.
+     */
+    public static final String ALL_WORKSPACES = "*";
+
+    @Property(value={"/apps", "/libs" })
+    public static final String PROP_PATH = "resource.resolver.searchpath";
+
+    /**
+     * Defines whether namespace prefixes of resource names inside the path
+     * (e.g. <code>jcr:</code> in <code>/home/path/jcr:content</code>) are
+     * mangled or not.
+     * <p>
+     * Mangling means that any namespace prefix contained in the path is replaced
+     * as per the generic substitution pattern <code>/([^:]+):/_$1_/</code>
+     * when calling the <code>map</code> method of the resource resolver.
+     * Likewise the <code>resolve</code> methods will unmangle such namespace
+     * prefixes according to the substituation pattern
+     * <code>/_([^_]+)_/$1:/</code>.
+     * <p>
+     * This feature is provided since there may be systems out there in the wild
+     * which cannot cope with URLs containing colons, even though they are
+     * perfectly valid characters in the path part of URI references with a
+     * scheme.
+     * <p>
+     * The default value of this property if no configuration is provided is
+     * <code>true</code>.
+     *
+     */
+    @Property(boolValue=true)
+    private static final String PROP_MANGLE_NAMESPACES = "resource.resolver.manglenamespaces";
+
+
+    @Property(boolValue=true)
+    private static final String PROP_ALLOW_DIRECT = "resource.resolver.allowDirect";
+
+    /**
+     * The resolver.virtual property has no default configuration. But the sling
+     * maven plugin and the sling management console cannot handle empty
+     * multivalue properties at the moment. So we just add a dummy direct
+     * mapping.
+     */
+    @Property(value="/:/", unbounded=PropertyUnbounded.ARRAY)
+    private static final String PROP_VIRTUAL = "resource.resolver.virtual";
+
+    @Property(value={"/:/", "/content/:/", "/system/docroot/:/"})
+    private static final String PROP_MAPPING = "resource.resolver.mapping";
+
+    @Property(value=MapEntries.DEFAULT_MAP_ROOT)
+    private static final String PROP_MAP_LOCATION = "resource.resolver.map.location";
+
+    @Property(boolValue=DEFAULT_MULTIWORKSPACE)
+    private static final String PROP_MULTIWORKSPACE = "resource.resolver.multiworkspace";
+
+    /** default log */
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    @Reference
+    private SlingRepository repository;
+
+    /** Tracker for the resource decorators. */
+    private final ResourceDecoratorTracker resourceDecoratorTracker = new ResourceDecoratorTracker();
+
+    // helper for the new JcrResourceResolver
+    private MapEntries mapEntries = MapEntries.EMPTY;
+
+    /** all mappings */
+    private Mapping[] mappings;
+
+    /** The fake urls */
+    private BidiMap virtualURLMap;
+
+    /** <code>true</code>, if direct mappings from URI to handle are allowed */
+    private boolean allowDirect = false;
+
+    // the search path for ResourceResolver.getResource(String)
+    private String[] searchPath;
+
+    // the root location of the /etc/map entries
+    private String mapRoot;
+
+    private final RootResourceProviderEntry rootProviderEntry;
+
+    // whether to mangle paths with namespaces or not
+    private boolean mangleNamespacePrefixes;
+
+    private boolean useMultiWorkspaces;
+
+    /** The resource listeners for the observation events. */
+    private Set<JcrResourceListener> resourceListeners;
+
+    /** The service tracker for the event admin
+     */
+    private ServiceTracker eventAdminTracker;
+
+    /** The dynamic class loader */
+    @Reference(cardinality=ReferenceCardinality.OPTIONAL_UNARY, policy=ReferencePolicy.DYNAMIC)
+    private DynamicClassLoaderManager dynamicClassLoaderManager;
+
+    private JcrItemAdapterFactory jcrItemAdapterFactory;
+
+    public JcrResourceResolverFactoryImpl() {
+        this.rootProviderEntry = new RootResourceProviderEntry();
+
+    }
+
+    public ResourceDecoratorTracker getResourceDecoratorTracker() {
+        return this.resourceDecoratorTracker;
+    }
+
+    // ---------- JcrResourceResolverFactory -----------------------------------
+
+    /**
+     * Returns a new <code>ResourceResolve</code> for the given session. Note
+     * that each call to this method returns a new resource manager instance.
+     *
+     * @see org.apache.sling.jcr.resource.JcrResourceResolverFactory#getResourceResolver(javax.jcr.Session)
+     */
+    public ResourceResolver getResourceResolver(Session session) {
+        Map<String, Object> authInfo = new HashMap<String, Object>(1);
+        authInfo.put(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, session);
+        try {
+            return getResourceResolver(authInfo);
+        } catch (LoginException le) {
+            // we don't expect a LoginException here because just a
+            // ResourceResolver wrapping the given session is to be created.
+            throw new InternalError("Unexpected LoginException");
+        }
+    }
+
+    // ---------- Resource Resolver Factory ------------------------------------
+
+    /**
+     * @see org.apache.sling.api.resource.ResourceResolverFactory#getAdministrativeResourceResolver(java.util.Map)
+     */
+    public ResourceResolver getAdministrativeResourceResolver(
+            final Map<String, Object> authenticationInfo) throws LoginException {
+        return getResourceResolverInternal(authenticationInfo, true);
+    }
+
+    /**
+     * @see org.apache.sling.api.resource.ResourceResolverFactory#getResourceResolver(java.util.Map)
+     */
+    public ResourceResolver getResourceResolver(
+            final Map<String, Object> authenticationInfo) throws LoginException {
+        return getResourceResolverInternal(authenticationInfo, false);
+    }
+
+    /**
+     * Create a new ResourceResolver wrapping a Session object. Carries map of
+     * authentication info in order to create a new resolver as needed.
+     */
+    private ResourceResolver getResourceResolverInternal(
+            final Map<String, Object> authenticationInfo, final boolean isAdmin)
+            throws LoginException {
+
+        // by default any session used by the resource resolver returned is
+        // closed when the resource resolver is closed
+        boolean logoutSession = true;
+
+        // derive the session to be used
+        Session session;
+        try {
+            final String workspace = getWorkspace(authenticationInfo);
+            if (isAdmin) {
+                // requested admin session to any workspace (or default)
+                session = getRepository().loginAdministrative(workspace);
+
+            } else {
+
+                session = getSession(authenticationInfo);
+                if (session == null) {
+                    // requested non-admin session to any workspace (or default)
+                    final Credentials credentials = getCredentials(authenticationInfo);
+                    session = getRepository().login(credentials, workspace);
+
+                } else if (workspace != null) {
+                    // session provided by map; but requested a different
+                    // workspace impersonate can only change the user not switch
+                    // the workspace as a workaround we login to the requested
+                    // workspace with admin and then switch to the provided
+                    // session's user (if required)
+                    Session tmpSession = null;
+                    try {
+                        tmpSession = getRepository().loginAdministrative(
+                            workspace);
+                        if (tmpSession.getUserID().equals(session.getUserID())) {
+                            session = tmpSession;
+                            tmpSession = null;
+                        } else {
+                            session = tmpSession.impersonate(new SimpleCredentials(
+                                session.getUserID(), new char[0]));
+                        }
+                    } finally {
+                        if (tmpSession != null) {
+                            tmpSession.logout();
+                        }
+                    }
+
+                } else {
+                    // session provided; no special workspace; just make sure
+                    // the session is not logged out when the resolver is closed
+                    logoutSession = false;
+                }
+            }
+        } catch (RepositoryException re) {
+            throw getLoginException(re);
+        }
+
+        session = handleImpersonation(session, authenticationInfo, logoutSession);
+
+        final JcrResourceProviderEntry sessionRoot = new JcrResourceProviderEntry(
+            session, rootProviderEntry, this.getDynamicClassLoader(),
+            useMultiWorkspaces);
+
+        if (logoutSession) {
+            return new JcrResourceResolver(sessionRoot, this, isAdmin,
+                authenticationInfo, useMultiWorkspaces);
+        }
+
+        return new JcrResourceResolver(sessionRoot, this, isAdmin,
+            authenticationInfo, useMultiWorkspaces) {
+            protected void closeSession() {
+            }
+        };
+    }
+
+    // ---------- Implementation helpers --------------------------------------
+
+    /** Get the dynamic class loader if available */
+    ClassLoader getDynamicClassLoader() {
+        final DynamicClassLoaderManager dclm = this.dynamicClassLoaderManager;
+        if ( dclm != null ) {
+            return dclm.getDynamicClassLoader();
+        }
+        return null;
+    }
+
+    /**
+     * This method is called from {@link MapEntries}
+     */
+    public BidiMap getVirtualURLMap() {
+        return virtualURLMap;
+    }
+
+    /**
+     * This method is called from {@link MapEntries}
+     */
+    public Mapping[] getMappings() {
+        return mappings;
+    }
+
+    String[] getSearchPath() {
+        return searchPath;
+    }
+
+    boolean isMangleNamespacePrefixes() {
+        return mangleNamespacePrefixes;
+
+    }
+
+    public String getMapRoot() {
+        return mapRoot;
+    }
+
+    MapEntries getMapEntries() {
+        return mapEntries;
+    }
+
+    String getDefaultWorkspaceName() {
+        return this.repository.getDefaultWorkspace();
+    }
+
+    /**
+     * Getter for rootProviderEntry, making it easier to extend
+     * JcrResourceResolverFactoryImpl. See <a
+     * href="https://issues.apache.org/jira/browse/SLING-730">SLING-730</a>
+     *
+     * @return Our rootProviderEntry
+     */
+    protected ResourceProviderEntry getRootProviderEntry() {
+        return rootProviderEntry;
+    }
+
+    // ---------- SCR Integration ---------------------------------------------
+
+    /** Activates this component, called by SCR before registering as a service */
+    protected void activate(final ComponentContext componentContext) {
+        // setup tracker first as this is used in the bind/unbind methods
+        this.eventAdminTracker = new ServiceTracker(componentContext.getBundleContext(),
+                EventAdmin.class.getName(), null);
+        this.eventAdminTracker.open();
+
+        final Dictionary<?, ?> properties = componentContext.getProperties();
+
+        BidiMap virtuals = new TreeBidiMap();
+        String[] virtualList = OsgiUtil.toStringArray(properties.get(PROP_VIRTUAL));
+        for (int i = 0; virtualList != null && i < virtualList.length; i++) {
+            String[] parts = Mapping.split(virtualList[i]);
+            virtuals.put(parts[0], parts[2]);
+        }
+        virtualURLMap = virtuals;
+
+        List<Mapping> maps = new ArrayList<Mapping>();
+        String[] mappingList = (String[]) properties.get(PROP_MAPPING);
+        for (int i = 0; mappingList != null && i < mappingList.length; i++) {
+            maps.add(new Mapping(mappingList[i]));
+        }
+        Mapping[] tmp = maps.toArray(new Mapping[maps.size()]);
+
+        // check whether direct mappings are allowed
+        Boolean directProp = (Boolean) properties.get(PROP_ALLOW_DIRECT);
+        allowDirect = (directProp != null) ? directProp.booleanValue() : true;
+        if (allowDirect) {
+            Mapping[] tmp2 = new Mapping[tmp.length + 1];
+            tmp2[0] = Mapping.DIRECT;
+            System.arraycopy(tmp, 0, tmp2, 1, tmp.length);
+            mappings = tmp2;
+        } else {
+            mappings = tmp;
+        }
+
+        // from configuration if available
+        searchPath = OsgiUtil.toStringArray(properties.get(PROP_PATH));
+        if (searchPath != null && searchPath.length > 0) {
+            for (int i = 0; i < searchPath.length; i++) {
+                // ensure leading slash
+                if (!searchPath[i].startsWith("/")) {
+                    searchPath[i] = "/" + searchPath[i];
+                }
+                // ensure trailing slash
+                if (!searchPath[i].endsWith("/")) {
+                    searchPath[i] += "/";
+                }
+            }
+        }
+        if (searchPath == null) {
+            searchPath = new String[] { "/" };
+        }
+
+        // namespace mangling
+        mangleNamespacePrefixes = OsgiUtil.toBoolean(
+            properties.get(PROP_MANGLE_NAMESPACES), false);
+
+        // the root of the resolver mappings
+        mapRoot = OsgiUtil.toString(properties.get(PROP_MAP_LOCATION),
+            MapEntries.DEFAULT_MAP_ROOT);
+
+        // set up the map entries from configuration
+        try {
+            mapEntries = new MapEntries(this, componentContext.getBundleContext(), this.eventAdminTracker);
+        } catch (Exception e) {
+            log.error(
+                "activate: Cannot access repository, failed setting up Mapping Support",
+                e);
+        }
+
+
+        // start observation listener
+        try {
+            this.resourceListeners = new HashSet<JcrResourceListener>();
+
+            // first - add a listener for the default workspace
+            this.resourceListeners.add(new JcrResourceListener(null, this, "/", "/", this.eventAdminTracker));
+
+            // check if multi workspace support is enabled
+            this.useMultiWorkspaces = OsgiUtil.toBoolean(properties.get(PROP_MULTIWORKSPACE), DEFAULT_MULTIWORKSPACE);
+            if (this.useMultiWorkspaces) {
+                final String[] listenerWorkspaces = getAllWorkspaces();
+                for (final String wspName : listenerWorkspaces) {
+                    if (!wspName.equals(this.repository.getDefaultWorkspace())) {
+                        this.resourceListeners.add(
+                            new JcrResourceListener(wspName, this, "/", "/", this.eventAdminTracker));
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error(
+                "activate: Cannot create resource listener; resource events for JCR resources will be disabled.",
+                e);
+        }
+
+        try {
+            plugin = new JcrResourceResolverWebConsolePlugin(componentContext.getBundleContext(), this);
+        } catch (Throwable ignore) {
+            // an exception here propably means the web console plugin is not available
+            log.debug(
+                    "activate: unable to setup web console plugin.", ignore);
+        }
+        
+        jcrItemAdapterFactory = new JcrItemAdapterFactory(componentContext.getBundleContext(), this);
+    }
+
+    private JcrResourceResolverWebConsolePlugin plugin;
+
+    /** Deativates this component, called by SCR to take out of service */
+    protected void deactivate(final ComponentContext componentContext) {
+        if (jcrItemAdapterFactory != null) {
+            jcrItemAdapterFactory.dispose();
+            jcrItemAdapterFactory = null;
+        }
+        
+        if (plugin != null) {
+            plugin.dispose();
+            plugin = null;
+        }
+
+        if (mapEntries != null) {
+            mapEntries.dispose();
+            mapEntries = MapEntries.EMPTY;
+        }
+        if ( this.eventAdminTracker != null ) {
+            this.eventAdminTracker.close();
+            this.eventAdminTracker = null;
+        }
+        if ( this.resourceListeners != null && !this.resourceListeners.isEmpty() ) {
+            for ( JcrResourceListener resourceListener : this.resourceListeners ) {
+                resourceListener.dispose();
+            }
+            this.resourceListeners = null;
+        }
+        this.resourceDecoratorTracker.close();
+    }
+
+    protected void bindResourceProvider(final ResourceProvider provider, final Map<String, Object> props) {
+        this.rootProviderEntry.bindResourceProvider(provider, props, this.eventAdminTracker);
+    }
+
+    protected void unbindResourceProvider(final ResourceProvider provider, final Map<String, Object> props) {
+        this.rootProviderEntry.unbindResourceProvider(provider, props, this.eventAdminTracker);
+    }
+
+    protected void bindResourceDecorator(final ResourceDecorator decorator, final Map<String, Object> props) {
+        this.resourceDecoratorTracker.bindResourceDecorator(decorator, props);
+    }
+
+    protected void unbindResourceDecorator(final ResourceDecorator decorator, final Map<String, Object> props) {
+        this.resourceDecoratorTracker.unbindResourceDecorator(decorator, props);
+    }
+
+    // ---------- internal helper ----------------------------------------------
+
+    /** Returns the JCR repository used by this factory */
+    protected SlingRepository getRepository() {
+        return repository;
+    }
+
+    /**
+     * Create a login exception from a repository exception.
+     * If the repository exception is a  {@link javax.jcr.LoginException}
+     * a {@link LoginException} is created with the same information.
+     * Otherwise a {@link LoginException} is created which wraps the
+     * repository exception.
+     * @param re The repository exception.
+     * @return The login exception.
+     */
+    private LoginException getLoginException(final RepositoryException re) {
+        if ( re instanceof javax.jcr.LoginException ) {
+            return new LoginException(re.getMessage(), re.getCause());
+        }
+        return new LoginException("Unable to login " + re.getMessage(), re);
+    }
+
+    /**
+     * Get an array of all workspaces.
+     */
+    private String[] getAllWorkspaces() throws RepositoryException {
+        Session session =  null;
+        try {
+            session = repository.loginAdministrative(null);
+            return session.getWorkspace().getAccessibleWorkspaceNames();
+        } finally {
+            if (session != null) {
+                session.logout();
+            }
+        }
+    }
+
+    /**
+     * Returns the session provided as the user.jcr.session property of the
+     * <code>authenticationInfo</code> map or <code>null</code> if the
+     * property is not contained in the map or is not a <code>javax.jcr.Session</code>.
+     * @param authenticationInfo Optional authentication info.
+     * @return The user.jcr.session property or <code>null</code>
+     */
+    private Session getSession(final Map<String, Object> authenticationInfo) {
+        if (authenticationInfo != null) {
+            final Object sessionObject = authenticationInfo.get(JcrResourceConstants.AUTHENTICATION_INFO_SESSION);
+            if (sessionObject instanceof Session) {
+                return (Session) sessionObject;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Return the workspace name.
+     * If the workspace name is provided, it is returned, otherwise
+     * <code>null</code> is returned.
+     * @param authenticationInfo Optional authentication info.
+     * @return The configured workspace name or <code>null</code>
+     */
+    private String getWorkspace(final Map<String, Object> authenticationInfo) {
+        if (authenticationInfo != null) {
+            final Object workspaceObject = authenticationInfo.get(JcrResourceConstants.AUTHENTICATION_INFO_WORKSPACE);
+            if (workspaceObject instanceof String) {
+                return (String) workspaceObject;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Return the sudo user information.
+     * If the sudo user info is provided, it is returned, otherwise
+     * <code>null</code> is returned.
+     * @param authenticationInfo Optional authentication info.
+     * @return The configured sudo user information or <code>null</code>
+     */
+    private String getSudoUser(final Map<String, Object> authenticationInfo) {
+        if (authenticationInfo != null) {
+            final Object sudoObject = authenticationInfo.get(ResourceResolverFactory.USER_IMPERSONATION);
+            if (sudoObject instanceof String) {
+                return (String) sudoObject;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Handle the sudo if configured. If the authentication info does not
+     * contain a sudo info, this method simply returns the passed in session. If
+     * a sudo user info is available, the session is tried to be impersonated.
+     * The new impersonated session is returned. The original session is closed.
+     * The session is also closed if the impersonation fails.
+     *
+     * @param session The session.
+     * @param authenticationInfo The optional authentication info.
+     * @param logoutSession whether to logout the <code>session</code> after
+     *            impersonation or not.
+     * @return The original session or impersonated session.
+     * @throws LoginException If something goes wrong.
+     */
+    private Session handleImpersonation(final Session session,
+            final Map<String, Object> authenticationInfo, boolean logoutSession)
+            throws LoginException {
+        final String sudoUser = getSudoUser(authenticationInfo);
+        if (sudoUser != null && !session.getUserID().equals(sudoUser)) {
+            try {
+                final SimpleCredentials creds = new SimpleCredentials(sudoUser,
+                    new char[0]);
+                copyAttributes(creds, authenticationInfo);
+                creds.setAttribute(ResourceResolver.USER_IMPERSONATOR,
+                    session.getUserID());
+                return session.impersonate(creds);
+            } catch (RepositoryException re) {
+                throw getLoginException(re);
+            } finally {
+                if (logoutSession) {
+                    session.logout();
+                }
+            }
+        }
+        return session;
+    }
+
+    /**
+     * Create a credentials object from the provided authentication info.
+     * If no map is provided, <code>null</code> is returned.
+     * If a map is provided and contains a credentials object, this object is
+     * returned.
+     * If a map is provided but does not contain a credentials object nor a
+     * user, <code>null</code> is returned.
+     * if a map is provided with a user name but without a credentials object
+     * a new credentials object is created and all values from the authentication
+     * info are added as attributes.
+     * @param authenticationInfo Optional authentication info
+     * @return A credentials object or <code>null</code>
+     */
+    private Credentials getCredentials(final Map<String, Object> authenticationInfo) {
+        if (authenticationInfo == null) {
+            return null;
+        }
+
+        final Object credentialsObject = authenticationInfo.get(JcrResourceConstants.AUTHENTICATION_INFO_CREDENTIALS);
+        if (credentialsObject instanceof Credentials) {
+            return (Credentials) credentialsObject;
+        }
+
+        // otherwise try to create SimpleCredentials if the userId is set
+        final Object userId = authenticationInfo.get(USER);
+        if (userId instanceof String) {
+            final Object password = authenticationInfo.get(PASSWORD);
+            final SimpleCredentials credentials = new SimpleCredentials(
+                (String) userId, ((password instanceof char[])
+                        ? (char[]) password
+                        : new char[0]));
+
+            // add attributes
+            copyAttributes(credentials, authenticationInfo);
+
+            return credentials;
+        }
+
+        // no user id (or not a String)
+        return null;
+    }
+
+    /**
+     * Copies the contents of the source map as attributes into the target
+     * <code>SimpleCredentials</code> object with the exception of the
+     * <code>user.jcr.credentials</code> and <code>user.password</code>
+     * attributes to prevent leaking passwords into the JCR Session attributes
+     * which might be used for break-in attempts.
+     *
+     * @param target The <code>SimpleCredentials</code> object whose attributes
+     *            are to be augmented.
+     * @param source The map whose entries (except the ones listed above) are
+     *            copied as credentials attributes.
+     */
+    private void copyAttributes(final SimpleCredentials target,
+            final Map<String, Object> source) {
+        final Iterator<Map.Entry<String, Object>> i = source.entrySet().iterator();
+        while (i.hasNext()) {
+            final Map.Entry<String, Object> current = i.next();
+            if (isAttributeVisible(current.getKey())) {
+                target.setAttribute(current.getKey(), current.getValue());
+            }
+        }
+    }
+
+    /**
+     * Returns <code>true</code> unless the name is
+     * <code>user.jcr.credentials</code> (
+     * {@link JcrResourceConstants#AUTHENTICATION_INFO_CREDENTIALS}) or contains
+     * the string <code>password</code> as in <code>user.password</code> (
+     * {@link org.apache.sling.api.resource.ResourceResolverFactory#PASSWORD})
+     *
+     * @param name The name to check whether it is visible or not
+     * @return <code>true</code> if the name is assumed visible
+     * @throws NullPointerException if <code>name</code> is <code>null</code>
+     */
+    static boolean isAttributeVisible(final String name) {
+        return !name.equals(JcrResourceConstants.AUTHENTICATION_INFO_CREDENTIALS)
+            && !name.contains("password");
+    }
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverImpl.java b/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverImpl.java
new file mode 100644
index 0000000..1f03d33
--- /dev/null
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverImpl.java
@@ -0,0 +1,1365 @@
+/*
+ * 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.resource.internal;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.jcr.Credentials;
+import javax.jcr.NamespaceException;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.Value;
+import javax.jcr.query.Query;
+import javax.jcr.query.QueryResult;
+import javax.jcr.query.Row;
+import javax.jcr.query.RowIterator;
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.sling.adapter.SlingAdaptable;
+import org.apache.sling.adapter.annotations.Adaptable;
+import org.apache.sling.adapter.annotations.Adapter;
+import org.apache.sling.api.SlingException;
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.NonExistingResource;
+import org.apache.sling.api.resource.QuerySyntaxException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceNotFoundException;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.jcr.resource.JcrResourceConstants;
+import org.apache.sling.jcr.resource.JcrResourceUtil;
+import org.apache.sling.jcr.resource.internal.helper.MapEntry;
+import org.apache.sling.jcr.resource.internal.helper.RedirectResource;
+import org.apache.sling.jcr.resource.internal.helper.ResourceIterator;
+import org.apache.sling.jcr.resource.internal.helper.ResourcePathIterator;
+import org.apache.sling.jcr.resource.internal.helper.URI;
+import org.apache.sling.jcr.resource.internal.helper.URIException;
+import org.apache.sling.jcr.resource.internal.helper.jcr.JcrNodeResourceIterator;
+import org.apache.sling.jcr.resource.internal.helper.jcr.JcrResourceProviderEntry;
+import org.apache.sling.jcr.resource.internal.helper.starresource.StarResource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Adaptable(adaptableClass=ResourceResolver.class, adapters={ @Adapter(Session.class) })
+public class JcrResourceResolver
+    extends SlingAdaptable implements ResourceResolver {
+
+    /** default logger */
+    private final Logger LOGGER = LoggerFactory.getLogger(JcrResourceResolver.class);
+
+    private static final String MANGLE_NAMESPACE_IN_SUFFIX = "_";
+
+    private static final String MANGLE_NAMESPACE_IN_PREFIX = "/_";
+
+    private static final String MANGLE_NAMESPACE_IN = "/_([^_/]+)_";
+
+    private static final String MANGLE_NAMESPACE_OUT_SUFFIX = ":";
+
+    private static final String MANGLE_NAMESPACE_OUT_PREFIX = "/";
+
+    private static final String MANGLE_NAMESPACE_OUT = "/([^:/]+):";
+
+    public static final String PROP_REG_EXP = "sling:match";
+
+    public static final String PROP_REDIRECT_INTERNAL = "sling:internalRedirect";
+
+    public static final String PROP_ALIAS = "sling:alias";
+
+    public static final String PROP_REDIRECT_EXTERNAL = "sling:redirect";
+
+    public static final String PROP_REDIRECT_EXTERNAL_STATUS = "sling:status";
+
+    public static final String PROP_REDIRECT_EXTERNAL_REDIRECT_STATUS = "sling:redirectStatus";
+
+    // The suffix of a resource being a content node of some parent
+    // such as nt:file. The slash is included to prevent false
+    // positives for the String.endsWith check for names like
+    // "xyzjcr:content"
+    private static final String JCR_CONTENT_LEAF = "/jcr:content";
+
+    @SuppressWarnings("deprecation")
+    private static final String DEFAULT_QUERY_LANGUAGE = Query.XPATH;
+
+    /** column name for node path */
+    private static final String QUERY_COLUMN_PATH = "jcr:path";
+
+    /** column name for score value */
+    private static final String QUERY_COLUMN_SCORE = "jcr:score";
+
+    /** The root provider for the resource tree. */
+    private final JcrResourceProviderEntry rootProvider;
+
+    /** The factory which created this resource resolver. */
+    private final JcrResourceResolverFactoryImpl factory;
+
+    /** Is this a resource resolver for an admin? */
+    private final boolean isAdmin;
+
+    /** The original authentication information - this is used for further resource resolver creations. */
+    private final Map<String, Object> originalAuthInfo;
+
+    /** Resolvers for different workspaces. */
+    private Map<String, JcrResourceResolver> createdResolvers;
+
+    /** Closed marker. */
+    private volatile boolean closed = false;
+
+    /** a resolver with the workspace which was specifically requested via a request attribute. */
+    private ResourceResolver requestBoundResolver;
+
+    private final boolean useMultiWorkspaces;
+
+    public JcrResourceResolver(final JcrResourceProviderEntry rootProvider,
+                               final JcrResourceResolverFactoryImpl factory,
+                               final boolean isAdmin,
+                               final Map<String, Object> originalAuthInfo,
+                               boolean useMultiWorkspaces) {
+        this.rootProvider = rootProvider;
+        this.factory = factory;
+        this.isAdmin = isAdmin;
+        this.originalAuthInfo = originalAuthInfo;
+        this.useMultiWorkspaces = useMultiWorkspaces;
+    }
+
+    /**
+     * @see org.apache.sling.api.resource.ResourceResolver#clone(Map)
+     */
+    public ResourceResolver clone(Map<String, Object> authenticationInfo)
+            throws LoginException {
+
+        // ensure resolver is still live
+        checkClosed();
+
+        // create the merged map
+        Map<String, Object> newAuthenticationInfo = new HashMap<String, Object>();
+        if (originalAuthInfo != null) {
+            newAuthenticationInfo.putAll(originalAuthInfo);
+        }
+        if (authenticationInfo != null) {
+            newAuthenticationInfo.putAll(authenticationInfo);
+        }
+
+        // get an administrative resolver if this resolver isAdmin unless
+        // credentials and/or user name are present in the credentials and/or
+        // a session is present
+        if (isAdmin
+            && !(newAuthenticationInfo.get(JcrResourceConstants.AUTHENTICATION_INFO_CREDENTIALS) instanceof Credentials)
+            && !(newAuthenticationInfo.get(JcrResourceConstants.AUTHENTICATION_INFO_SESSION) instanceof Session)
+            && !(newAuthenticationInfo.get(ResourceResolverFactory.USER) instanceof String)) {
+            return factory.getAdministrativeResourceResolver(newAuthenticationInfo);
+        }
+
+        // create a regular resource resolver
+        return factory.getResourceResolver(newAuthenticationInfo);
+    }
+
+    /**
+     * @see org.apache.sling.api.resource.ResourceResolver#isLive()
+     */
+    public boolean isLive() {
+        return !this.closed && getSession().isLive();
+    }
+
+    /**
+     * @see org.apache.sling.api.resource.ResourceResolver#close()
+     */
+    public void close() {
+        if (!this.closed) {
+            this.closed = true;
+            closeCreatedResolvers();
+            closeSession();
+        }
+    }
+
+    /**
+     * Closes the session underlying this resource resolver. This method is
+     * called by the {@link #close()} method.
+     * <p>
+     * Extensions can overwrite this method to do other work (or not close the
+     * session at all). Handle with care !
+     */
+    protected void closeSession() {
+        try {
+            getSession().logout();
+        } catch (Throwable t) {
+            LOGGER.debug(
+                "closeSession: Unexpected problem closing the session; ignoring",
+                t);
+        }
+    }
+
+    /**
+     * Closes any helper resource resolver created while this resource resolver
+     * was used.
+     * <p>
+     * Extensions can overwrite this method to do other work (or not close the
+     * created resource resovlers at all). Handle with care !
+     */
+    protected void closeCreatedResolvers() {
+        if (this.createdResolvers != null) {
+            for (final ResourceResolver resolver : createdResolvers.values()) {
+                try {
+                    resolver.close();
+                } catch (Throwable t) {
+                    LOGGER.debug(
+                        "closeCreatedResolvers: Unexpected problem closing the created resovler "
+                            + resolver + "; ignoring", t);
+                }
+            }
+        }
+    }
+
+    /**
+     * Calls the {@link #close()} method to ensure the resolver is properly
+     * cleaned up before it is being collected by the garbage collector because
+     * it is not referred to any more.
+     */
+    protected void finalize() {
+        close();
+    }
+
+    /**
+     * Check if the resource resolver is already closed.
+     *
+     * @throws IllegalStateException If the resolver is already closed
+     */
+    private void checkClosed() {
+        if ( this.closed ) {
+            throw new IllegalStateException("Resource resolver is already closed.");
+        }
+    }
+
+    // ---------- attributes
+
+    /**
+     * @see org.apache.sling.api.resource.ResourceResolver#getAttributeNames()
+     */
+    public Iterator<String> getAttributeNames() {
+        checkClosed();
+        final Set<String> names = new HashSet<String>();
+        names.addAll(Arrays.asList(getSession().getAttributeNames()));
+        if (originalAuthInfo != null) {
+            names.addAll(originalAuthInfo.keySet());
+        }
+        return new Iterator<String>() {
+            final Iterator<String> keys = names.iterator();
+
+            String nextKey = seek();
+
+            private String seek() {
+                while (keys.hasNext()) {
+                    final String key = keys.next();
+                    if (JcrResourceResolverFactoryImpl.isAttributeVisible(key)) {
+                        return key;
+                    }
+                }
+                return null;
+            }
+
+            public boolean hasNext() {
+                return nextKey != null;
+            }
+
+            public String next() {
+                if (!hasNext()) {
+                    throw new NoSuchElementException();
+                }
+                String toReturn = nextKey;
+                nextKey = seek();
+                return toReturn;
+            }
+
+            public void remove() {
+                throw new UnsupportedOperationException("remove");
+            }
+        };
+    }
+
+    /**
+     * @see org.apache.sling.api.resource.ResourceResolver#getAttribute(String)
+     */
+    public Object getAttribute(String name) {
+        if (name == null) {
+            throw new NullPointerException("name");
+        }
+
+        if (JcrResourceResolverFactoryImpl.isAttributeVisible(name)) {
+            final Object sessionAttr = getSession().getAttribute(name);
+            if (sessionAttr != null) {
+                return sessionAttr;
+            }
+            if (originalAuthInfo != null) {
+                return originalAuthInfo.get(name);
+            }
+        }
+
+        // not a visible attribute
+        return null;
+    }
+
+    // ---------- resolving resources
+
+    /**
+     * @see org.apache.sling.api.resource.ResourceResolver#resolve(java.lang.String)
+     */
+    public Resource resolve(String absPath) {
+        checkClosed();
+        return resolve(null, absPath);
+    }
+
+    /**
+     * @see org.apache.sling.api.resource.ResourceResolver#resolve(javax.servlet.http.HttpServletRequest)
+     */
+    public Resource resolve(HttpServletRequest request) {
+        checkClosed();
+        // throws NPE if request is null as required
+        return resolve(request, request.getPathInfo());
+    }
+
+    /**
+     * @see org.apache.sling.api.resource.ResourceResolver#resolve(javax.servlet.http.HttpServletRequest, java.lang.String)
+     */
+    public Resource resolve(final HttpServletRequest request, String absPath) {
+        checkClosed();
+
+        String workspaceName = null;
+
+        // make sure abspath is not null and is absolute
+        if (absPath == null) {
+            absPath = "/";
+        } else if (!absPath.startsWith("/")) {
+            if (useMultiWorkspaces) {
+                final int wsSepPos = absPath.indexOf(":/");
+                if (wsSepPos != -1) {
+                    workspaceName = absPath.substring(0, wsSepPos);
+                    absPath = absPath.substring(wsSepPos + 1);
+                } else {
+                    absPath = "/" + absPath;
+                }
+            } else {
+                absPath = "/" + absPath;
+            }
+        }
+
+        // check for special namespace prefix treatment
+        absPath = unmangleNamespaces(absPath);
+        if (useMultiWorkspaces) {
+            if (workspaceName == null) {
+                // check for workspace info from request
+                workspaceName = (request == null ? null :
+                    (String)request.getAttribute(ResourceResolver.REQUEST_ATTR_WORKSPACE_INFO));
+            }
+            if (workspaceName != null && !workspaceName.equals(getSession().getWorkspace().getName())) {
+                LOGGER.debug("Delegating resolving to resolver for workspace {}", workspaceName);
+                try {
+                    final ResourceResolver wsResolver = getResolverForWorkspace(workspaceName);
+                    requestBoundResolver = wsResolver;
+                    return wsResolver.resolve(request, absPath);
+                } catch (LoginException e) {
+                    // requested a resource in a workspace I don't have access to.
+                    // we treat this as a not found resource
+                    LOGGER.debug(
+                        "resolve: Path {} does not resolve, returning NonExistingResource",
+                           absPath);
+
+                    final Resource res = new NonExistingResource(this, absPath);
+                    // SLING-864: if the path contains a dot we assume this to be
+                    // the start for any selectors, extension, suffix, which may be
+                    // used for further request processing.
+                    int index = absPath.indexOf('.');
+                    if (index != -1) {
+                        res.getResourceMetadata().setResolutionPathInfo(absPath.substring(index));
+                    }
+                    return this.factory.getResourceDecoratorTracker().decorate(res, workspaceName);
+                }
+
+            }
+        }
+        // Assume http://localhost:80 if request is null
+        String[] realPathList = { absPath };
+        String requestPath;
+        if (request != null) {
+            requestPath = getMapPath(request.getScheme(),
+                request.getServerName(), request.getServerPort(), absPath);
+        } else {
+            requestPath = getMapPath("http", "localhost", 80, absPath);
+        }
+
+        LOGGER.debug("resolve: Resolving request path {}", requestPath);
+
+        // loop while finding internal or external redirect into the
+        // content out of the virtual host mapping tree
+        // the counter is to ensure we are not caught in an endless loop here
+        // TODO: might do better to be able to log the loop and help the user
+        for (int i = 0; i < 100; i++) {
+
+            String[] mappedPath = null;
+
+            final Iterator<MapEntry> mapEntriesIterator = this.factory.getMapEntries().getResolveMapsIterator(requestPath);
+            while ( mapEntriesIterator.hasNext() ) {
+                final MapEntry mapEntry = mapEntriesIterator.next();
+                mappedPath = mapEntry.replace(requestPath);
+                if (mappedPath != null) {
+                    if ( LOGGER.isDebugEnabled() ) {
+                        LOGGER.debug(
+                            "resolve: MapEntry {} matches, mapped path is {}",
+                            mapEntry, Arrays.toString(mappedPath));
+                    }
+                    if (mapEntry.isInternal()) {
+                        // internal redirect
+                        LOGGER.debug("resolve: Redirecting internally");
+                        break;
+                    }
+
+                    // external redirect
+                    LOGGER.debug("resolve: Returning external redirect");
+                    return this.factory.getResourceDecoratorTracker().decorate(
+                            new RedirectResource(this, absPath, mappedPath[0],
+                                   mapEntry.getStatus()), workspaceName);
+                }
+            }
+
+            // if there is no virtual host based path mapping, abort
+            // and use the original realPath
+            if (mappedPath == null) {
+                LOGGER.debug(
+                    "resolve: Request path {} does not match any MapEntry",
+                    requestPath);
+                break;
+            }
+
+            // if the mapped path is not an URL, use this path to continue
+            if (!mappedPath[0].contains("://")) {
+                LOGGER.debug("resolve: Mapped path is for resource tree");
+                realPathList = mappedPath;
+                break;
+            }
+
+            // otherwise the mapped path is an URI and we have to try to
+            // resolve that URI now, using the URI's path as the real path
+            try {
+                final URI uri = new URI(mappedPath[0], false);
+                requestPath = getMapPath(uri.getScheme(), uri.getHost(),
+                    uri.getPort(), uri.getPath());
+                realPathList = new String[] { uri.getPath() };
+
+                LOGGER.debug(
+                    "resolve: Mapped path is an URL, using new request path {}",
+                    requestPath);
+            } catch (final URIException use) {
+                // TODO: log and fail
+                throw new ResourceNotFoundException(absPath);
+            }
+        }
+
+        // now we have the real path resolved from virtual host mapping
+        // this path may be absolute or relative, in which case we try
+        // to resolve it against the search path
+
+        Resource res = null;
+        for (int i = 0; res == null && i < realPathList.length; i++) {
+            String realPath = realPathList[i];
+
+            // first check whether the requested resource is a StarResource
+            if (StarResource.appliesTo(realPath)) {
+                LOGGER.debug("resolve: Mapped path {} is a Star Resource",
+                    realPath);
+                res = new StarResource(this, ensureAbsPath(realPath));
+
+            } else {
+
+                if (realPath.startsWith("/")) {
+
+                    // let's check it with a direct access first
+                    LOGGER.debug("resolve: Try absolute mapped path {}", realPath);
+                    res = resolveInternal(realPath);
+
+                } else {
+
+                    String[] searchPath = getSearchPath();
+                    for (int spi = 0; res == null && spi < searchPath.length; spi++) {
+                        LOGGER.debug(
+                            "resolve: Try relative mapped path with search path entry {}",
+                            searchPath[spi]);
+                        res = resolveInternal(searchPath[spi] + realPath);
+                    }
+
+                }
+            }
+
+        }
+
+        // if no resource has been found, use a NonExistingResource
+        if (res == null) {
+            String resourcePath = ensureAbsPath(realPathList[0]);
+            LOGGER.debug(
+                "resolve: Path {} does not resolve, returning NonExistingResource at {}",
+                   absPath, resourcePath);
+
+            res = new NonExistingResource(this, resourcePath);
+            // SLING-864: if the path contains a dot we assume this to be
+            // the start for any selectors, extension, suffix, which may be
+            // used for further request processing.
+            int index = resourcePath.indexOf('.');
+            if (index != -1) {
+                res.getResourceMetadata().setResolutionPathInfo(resourcePath.substring(index));
+            }
+        } else {
+            LOGGER.debug("resolve: Path {} resolves to Resource {}", absPath, res);
+        }
+
+        return this.factory.getResourceDecoratorTracker().decorate(res, workspaceName);
+    }
+
+    /**
+     * calls map(HttpServletRequest, String) as map(null, resourcePath)
+     * @see org.apache.sling.api.resource.ResourceResolver#map(java.lang.String)
+     */
+    public String map(final String resourcePath) {
+        checkClosed();
+        return map(null, resourcePath);
+    }
+
+    /**
+     * full implementation
+     *   - apply sling:alias from the resource path
+     *   - apply /etc/map mappings (inkl. config backwards compat)
+     *   - return absolute uri if possible
+     * @see org.apache.sling.api.resource.ResourceResolver#map(javax.servlet.http.HttpServletRequest, java.lang.String)
+     */
+    public String map(final HttpServletRequest request, final String resourcePath) {
+        checkClosed();
+
+        // find a fragment or query
+        int fragmentQueryMark = resourcePath.indexOf('#');
+        if (fragmentQueryMark < 0) {
+            fragmentQueryMark = resourcePath.indexOf('?');
+        }
+
+        // cut fragment or query off the resource path
+        String mappedPath;
+        final String fragmentQuery;
+        if (fragmentQueryMark >= 0) {
+            fragmentQuery = resourcePath.substring(fragmentQueryMark);
+            mappedPath = resourcePath.substring(0, fragmentQueryMark);
+            LOGGER.debug("map: Splitting resource path '{}' into '{}' and '{}'",
+                new Object[] { resourcePath, mappedPath, fragmentQuery });
+        } else {
+            fragmentQuery = null;
+            mappedPath = resourcePath;
+        }
+
+
+        // cut off scheme and host, if the same as requested
+        final String schemehostport;
+        final String schemePrefix;
+        if (request != null) {
+            schemehostport = MapEntry.getURI(request.getScheme(),
+                request.getServerName(), request.getServerPort(), "/");
+            schemePrefix = request.getScheme().concat("://");
+            LOGGER.debug(
+                "map: Mapping path {} for {} (at least with scheme prefix {})",
+                new Object[] { resourcePath, schemehostport, schemePrefix });
+
+        } else {
+
+            schemehostport = null;
+            schemePrefix = null;
+            LOGGER.debug("map: Mapping path {} for default", resourcePath);
+
+        }
+
+        Resource res = null;
+        String workspaceName = null;
+
+        if (useMultiWorkspaces) {
+            final int wsSepPos = mappedPath.indexOf(":/");
+            if (wsSepPos != -1) {
+                workspaceName = mappedPath.substring(0, wsSepPos);
+                if (workspaceName.equals(getSession().getWorkspace().getName())) {
+                    mappedPath = mappedPath.substring(wsSepPos + 1);
+                } else {
+                    try {
+                        JcrResourceResolver wsResolver = getResolverForWorkspace(workspaceName);
+                        mappedPath = mappedPath.substring(wsSepPos + 1);
+                        res = wsResolver.resolveInternal(mappedPath);
+                    } catch (LoginException e) {
+                        // requested a resource in a workspace I don't have access to.
+                        // we treat this as a not found resource
+                        return null;
+                    }
+                }
+            } else {
+                // check for workspace info in request
+                workspaceName = (request == null ? null :
+                    (String)request.getAttribute(ResourceResolver.REQUEST_ATTR_WORKSPACE_INFO));
+                if ( workspaceName != null && !workspaceName.equals(getSession().getWorkspace().getName())) {
+                    LOGGER.debug("Delegating resolving to resolver for workspace {}", workspaceName);
+                    try {
+                        JcrResourceResolver wsResolver = getResolverForWorkspace(workspaceName);
+                        res = wsResolver.resolveInternal(mappedPath);
+                    } catch (LoginException e) {
+                        // requested a resource in a workspace I don't have access to.
+                        // we treat this as a not found resource
+                        return null;
+                    }
+
+                }
+            }
+        }
+
+        if (res == null) {
+            res = resolveInternal(mappedPath);
+        }
+
+        if (res != null) {
+
+            // keep, what we might have cut off in internal resolution
+            final String resolutionPathInfo = res.getResourceMetadata().getResolutionPathInfo();
+
+            LOGGER.debug("map: Path maps to resource {} with path info {}", res,
+                resolutionPathInfo);
+
+            // find aliases for segments. we can't walk the parent chain
+            // since the request session might not have permissions to
+            // read all parents SLING-2093
+            final LinkedList<String> names = new LinkedList<String>();
+
+            Resource current = res;
+            String path = res.getPath();
+            while ( path != null ) {
+                String alias = null;
+                if ( current != null && !path.endsWith(JCR_CONTENT_LEAF)) {
+                    alias = getProperty(current, PROP_ALIAS);
+                }
+                if (alias == null || alias.length() == 0) {
+                    alias = ResourceUtil.getName(path);
+                }
+                names.add(alias);
+                path = ResourceUtil.getParent(path);
+                if ( "/".equals(path) ) {
+                    path = null;
+                } else if ( path != null ) {
+                    current = res.getResourceResolver().resolve(path);
+                }
+            }
+
+            // build path from segment names
+            final StringBuilder buf = new StringBuilder();
+
+            // construct the path from the segments (or root if none)
+            if (names.isEmpty()) {
+                buf.append('/');
+            } else {
+                while (!names.isEmpty()) {
+                    buf.append('/');
+                    buf.append(names.removeLast());
+                }
+            }
+
+            // reappend the resolutionPathInfo
+            if (resolutionPathInfo != null) {
+                buf.append(resolutionPathInfo);
+            }
+
+            // and then we have the mapped path to work on
+            mappedPath = buf.toString();
+
+            LOGGER.debug("map: Alias mapping resolves to path {}", mappedPath);
+
+        }
+
+        boolean mappedPathIsUrl = false;
+        for (final MapEntry mapEntry : this.factory.getMapEntries().getMapMaps()) {
+            final String[] mappedPaths = mapEntry.replace(mappedPath);
+            if (mappedPaths != null) {
+
+                LOGGER.debug("map: Match for Entry {}", mapEntry);
+
+                mappedPathIsUrl = !mapEntry.isInternal();
+
+                if (mappedPathIsUrl && schemehostport != null) {
+
+                    mappedPath = null;
+
+                    for (final String candidate : mappedPaths) {
+                        if (candidate.startsWith(schemehostport)) {
+                            mappedPath = candidate.substring(schemehostport.length() - 1);
+                            mappedPathIsUrl = false;
+                            LOGGER.debug(
+                                "map: Found host specific mapping {} resolving to {}",
+                                candidate, mappedPath);
+                            break;
+                        } else if (candidate.startsWith(schemePrefix)
+                            && mappedPath == null) {
+                            mappedPath = candidate;
+                        }
+                    }
+
+                    if (mappedPath == null) {
+                        mappedPath = mappedPaths[0];
+                    }
+
+                } else {
+
+                    // we can only go with assumptions selecting the first entry
+                    mappedPath = mappedPaths[0];
+
+                }
+
+                LOGGER.debug(
+                    "resolve: MapEntry {} matches, mapped path is {}",
+                    mapEntry, mappedPath);
+
+                break;
+            }
+        }
+
+        // this should not be the case, since mappedPath is primed
+        if (mappedPath == null) {
+            mappedPath = resourcePath;
+        }
+
+        // [scheme:][//authority][path][?query][#fragment]
+        try {
+            // use commons-httpclient's URI instead of java.net.URI, as it can
+            // actually accept *unescaped* URIs, such as the "mappedPath" and
+            // return them in proper escaped form, including the path, via toString()
+            URI uri = new URI(mappedPath, false);
+
+            // 1. mangle the namespaces in the path
+            String path = mangleNamespaces(uri.getPath());
+
+            // 2. prepend servlet context path if we have a request
+            if (request != null && request.getContextPath() != null
+                && request.getContextPath().length() > 0) {
+                path = request.getContextPath().concat(path);
+            }
+            // update the path part of the URI
+            uri.setPath(path);
+
+            mappedPath = uri.toString();
+        } catch (URIException e) {
+            LOGGER.warn("map: Unable to mangle namespaces for " + mappedPath
+                    + " returning unmangled", e);
+        }
+
+        LOGGER.debug("map: Returning URL {} as mapping for path {}",
+            mappedPath, resourcePath);
+
+        // reappend fragment and/or query
+        if (fragmentQuery != null) {
+            mappedPath = mappedPath.concat(fragmentQuery);
+        }
+
+        return mappedPath;
+    }
+
+    // ---------- search path for relative resoures
+
+    /**
+     * @see org.apache.sling.api.resource.ResourceResolver#getSearchPath()
+     */
+    public String[] getSearchPath() {
+        checkClosed();
+        return factory.getSearchPath().clone();
+    }
+
+    // ---------- direct resource access without resolution
+
+    /**
+     * @see org.apache.sling.api.resource.ResourceResolver#getResource(java.lang.String)
+     */
+    public Resource getResource(String path) {
+        checkClosed();
+
+        if (useMultiWorkspaces) {
+            final int wsSepPos = path.indexOf(":/");
+            if (wsSepPos != -1) {
+                final String workspaceName = path.substring(0, wsSepPos);
+                if (workspaceName.equals(getSession().getWorkspace().getName())) {
+                    path = path.substring(wsSepPos + 1);
+                } else {
+                    try {
+                        ResourceResolver wsResolver = getResolverForWorkspace(workspaceName);
+                        return wsResolver.getResource(path.substring(wsSepPos + 1));
+                    } catch (LoginException e) {
+                        // requested a resource in a workspace I don't have access to.
+                        // we treat this as a not found resource
+                        return null;
+                    }
+                }
+            }
+        }
+
+        // if the path is absolute, normalize . and .. segements and get res
+        if (path.startsWith("/")) {
+            path = ResourceUtil.normalize(path);
+            Resource result = (path != null) ? getResourceInternal(path) : null;
+            if ( result != null ) {
+                String workspacePrefix = null;
+                if ( useMultiWorkspaces && !getSession().getWorkspace().getName().equals(this.factory.getDefaultWorkspaceName()) ) {
+                    workspacePrefix = getSession().getWorkspace().getName();
+                }
+
+                result = this.factory.getResourceDecoratorTracker().decorate(result, workspacePrefix);
+                return result;
+            }
+            return null;
+        }
+
+        // otherwise we have to apply the search path
+        // (don't use this.getSearchPath() to save a few cycle for not cloning)
+        String[] paths = factory.getSearchPath();
+        if (paths != null) {
+            for (String prefix : factory.getSearchPath()) {
+                Resource res = getResource(prefix + path);
+                if (res != null) {
+                    return res;
+                }
+            }
+        }
+
+        // no resource found, if we get here
+        return null;
+    }
+
+    /**
+     * @see org.apache.sling.api.resource.ResourceResolver#getResource(org.apache.sling.api.resource.Resource, java.lang.String)
+     */
+    public Resource getResource(Resource base, String path) {
+        checkClosed();
+
+        if (!path.startsWith("/") && base != null) {
+            path = base.getPath() + "/" + path;
+        }
+
+        return getResource(path);
+    }
+
+    /**
+     * @see org.apache.sling.api.resource.ResourceResolver#listChildren(org.apache.sling.api.resource.Resource)
+     */
+    @SuppressWarnings("unchecked")
+    public Iterator<Resource> listChildren(Resource parent) {
+        checkClosed();
+        final String path = parent.getPath();
+        final int wsSepPos = path.indexOf(":/");
+        if (wsSepPos != -1) {
+            final String workspaceName = path.substring(0, wsSepPos);
+            if (!workspaceName.equals(getSession().getWorkspace().getName())) {
+                if (useMultiWorkspaces) {
+                    try {
+                        ResourceResolver wsResolver = getResolverForWorkspace(workspaceName);
+                        return wsResolver.listChildren(parent);
+                    } catch (LoginException e) {
+                        // requested a resource in a workspace I don't have access to.
+                        // we treat this as a not found resource
+                        return Collections.EMPTY_LIST.iterator();
+                    }
+                }
+                // this is illegal
+                return Collections.EMPTY_LIST.iterator();
+            } else if (parent instanceof WorkspaceDecoratedResource) {
+                parent = ((WorkspaceDecoratedResource) parent).getResource();
+            } else {
+                LOGGER.warn("looking for children of workspace path {}, but with an undecorated resource.",
+                        parent.getPath());
+            }
+        }
+
+        String workspacePrefix = null;
+        if ( useMultiWorkspaces && !getSession().getWorkspace().getName().equals(this.factory.getDefaultWorkspaceName()) ) {
+            workspacePrefix = getSession().getWorkspace().getName();
+        }
+
+        return new ResourceIteratorDecorator(
+            this.factory.getResourceDecoratorTracker(), workspacePrefix,
+            new ResourceIterator(parent, rootProvider));
+    }
+
+    // ---------- Querying resources
+
+    /**
+     * @see org.apache.sling.api.resource.ResourceResolver#findResources(java.lang.String, java.lang.String)
+     */
+    public Iterator<Resource> findResources(final String query, final String language)
+    throws SlingException {
+        checkClosed();
+        try {
+            Session session = null;
+            String workspaceName = null;
+            if (requestBoundResolver != null) {
+                session = requestBoundResolver.adaptTo(Session.class);
+                workspaceName = session.getWorkspace().getName();
+            } else {
+                session = getSession();
+            }
+            final QueryResult res = JcrResourceUtil.query(session, query, language);
+            return new ResourceIteratorDecorator(this.factory.getResourceDecoratorTracker(), workspaceName,
+                    new JcrNodeResourceIterator(this, res.getNodes(), factory.getDynamicClassLoader()));
+        } catch (javax.jcr.query.InvalidQueryException iqe) {
+            throw new QuerySyntaxException(iqe.getMessage(), query, language, iqe);
+        } catch (RepositoryException re) {
+            throw new SlingException(re.getMessage(), re);
+        }
+    }
+
+    /**
+     * @see org.apache.sling.api.resource.ResourceResolver#queryResources(java.lang.String, java.lang.String)
+     */
+    public Iterator<Map<String, Object>> queryResources(final String query,
+                                                        final String language)
+    throws SlingException {
+        checkClosed();
+
+        final String queryLanguage = isSupportedQueryLanguage(language) ? language : DEFAULT_QUERY_LANGUAGE;
+
+        try {
+            QueryResult result = JcrResourceUtil.query(adaptTo(Session.class), query,
+                queryLanguage);
+            final String[] colNames = result.getColumnNames();
+            final RowIterator rows = result.getRows();
+            return new Iterator<Map<String, Object>>() {
+                public boolean hasNext() {
+                    return rows.hasNext();
+                };
+
+                public Map<String, Object> next() {
+                    Map<String, Object> row = new HashMap<String, Object>();
+                    try {
+                        Row jcrRow = rows.nextRow();
+                        boolean didPath = false;
+                        boolean didScore = false;
+                        Value[] values = jcrRow.getValues();
+                        for (int i = 0; i < values.length; i++) {
+                            Value v = values[i];
+                            if (v != null) {
+                                String colName = colNames[i];
+                                row.put(colName,
+                                    JcrResourceUtil.toJavaObject(values[i]));
+                                if (colName.equals(QUERY_COLUMN_PATH)) {
+                                    didPath = true;
+                                }
+                                if (colName.equals(QUERY_COLUMN_SCORE)) {
+                                    didScore = true;
+                                }
+                            }
+                        }
+                        if (!didPath) {
+                            row.put(QUERY_COLUMN_PATH, jcrRow.getPath());
+                        }
+                        if (!didScore) {
+                            row.put(QUERY_COLUMN_SCORE, jcrRow.getScore());
+                        }
+
+                    } catch (RepositoryException re) {
+                        LOGGER.error(
+                            "queryResources$next: Problem accessing row values",
+                            re);
+                    }
+                    return row;
+                }
+
+                public void remove() {
+                    throw new UnsupportedOperationException("remove");
+                }
+            };
+        } catch (javax.jcr.query.InvalidQueryException iqe) {
+            throw new QuerySyntaxException(iqe.getMessage(), query, language,
+                iqe);
+        } catch (RepositoryException re) {
+            throw new SlingException(re.getMessage(), re);
+        }
+    }
+
+    /**
+     * @see org.apache.sling.api.resource.ResourceResolver#getUserID()
+     */
+    public String getUserID() {
+        checkClosed();
+        return getSession().getUserID();
+    }
+
+    // ---------- Adaptable interface
+
+    /**
+     * @see org.apache.sling.adapter.SlingAdaptable#adaptTo(java.lang.Class)
+     */
+    @SuppressWarnings("unchecked")
+    public <AdapterType> AdapterType adaptTo(Class<AdapterType> type) {
+        checkClosed();
+        if (type == Session.class) {
+            if (requestBoundResolver != null) {
+                return (AdapterType) requestBoundResolver.adaptTo(Session.class);
+            }
+            return (AdapterType) getSession();
+        }
+
+        // fall back to default behaviour
+        return super.adaptTo(type);
+    }
+
+    // ---------- internal
+
+    /**
+     * Returns the JCR Session of the root resource provider which provides
+     * access to the repository.
+     */
+    private Session getSession() {
+        return rootProvider.getSession();
+    }
+
+    /**
+     * Get a resolver for the workspace.
+     */
+    private synchronized JcrResourceResolver getResolverForWorkspace(
+            final String workspaceName) throws LoginException {
+        if (createdResolvers == null) {
+            createdResolvers = new HashMap<String, JcrResourceResolver>();
+        }
+        JcrResourceResolver wsResolver = createdResolvers.get(workspaceName);
+        if (wsResolver == null) {
+            final Map<String, Object> newAuthInfo = new HashMap<String, Object>();
+            newAuthInfo.put(JcrResourceConstants.AUTHENTICATION_INFO_WORKSPACE,
+                workspaceName);
+            wsResolver = (JcrResourceResolver) clone(newAuthInfo);
+            createdResolvers.put(workspaceName, wsResolver);
+        }
+        return wsResolver;
+    }
+
+    /**
+     * Returns a string used for matching map entries against the given request
+     * or URI parts.
+     *
+     * @param scheme The URI scheme
+     * @param host The host name
+     * @param port The port number. If this is negative, the default value used
+     *            is 80 unless the scheme is "https" in which case the default
+     *            value is 443.
+     * @param path The (absolute) path
+     * @return The request path string {scheme}/{host}.{port}{path}.
+     */
+    public static String getMapPath(String scheme, String host, int port, String path) {
+        if (port < 0) {
+            port = ("https".equals(scheme)) ? 443 : 80;
+        }
+
+        return scheme + "/" + host + "." + port + path;
+    }
+
+    /**
+     * Internally resolves the absolute path. The will almost always contain
+     * request selectors and an extension. Therefore this method uses the
+     * {@link ResourcePathIterator} to cut off parts of the path to find the
+     * actual resource.
+     * <p>
+     * This method operates in two steps:
+     * <ol>
+     * <li>Check the path directly
+     * <li>Drill down the resource tree from the root down to the resource
+     * trying to get the child as per the respective path segment or finding a
+     * child whose <code>sling:alias</code> property is set to the respective
+     * name.
+     * </ol>
+     * <p>
+     * If neither mechanism (direct access and drill down) resolves to a
+     * resource this method returns <code>null</code>.
+     *
+     * @param absPath The absolute path of the resource to return.
+     * @return The resource found or <code>null</code> if the resource could
+     *         not be found. The
+     *         {@link org.apache.sling.api.resource.ResourceMetadata#getResolutionPathInfo() resolution path info}
+     *         field of the resource returned is set to the part of the
+     *         <code>absPath</code> which has been cut off by the
+     *         {@link ResourcePathIterator} to resolve the resource.
+     */
+    private Resource resolveInternal(String absPath) {
+        Resource resource = null;
+        String curPath = absPath;
+        try {
+            final ResourcePathIterator it = new ResourcePathIterator(absPath);
+            while (it.hasNext() && resource == null) {
+                curPath = it.next();
+                resource = getResourceInternal(curPath);
+            }
+        } catch (Exception ex) {
+            throw new SlingException("Problem trying " + curPath
+                + " for request path " + absPath, ex);
+        }
+
+        // SLING-627: set the part cut off from the uriPath as
+        // sling.resolutionPathInfo property such that
+        // uriPath = curPath + sling.resolutionPathInfo
+        if (resource != null) {
+
+            String rpi = absPath.substring(curPath.length());
+            resource.getResourceMetadata().setResolutionPathInfo(rpi);
+
+            LOGGER.debug(
+                "resolveInternal: Found resource {} with path info {} for {}",
+                new Object[] { resource, rpi, absPath });
+
+        } else {
+
+            // no direct resource found, so we have to drill down into the
+            // resource tree to find a match
+            resource = getResourceInternal("/");
+            StringBuilder resolutionPath = new StringBuilder();
+            StringTokenizer tokener = new StringTokenizer(absPath, "/");
+            while (resource != null && tokener.hasMoreTokens()) {
+                String childNameRaw = tokener.nextToken();
+
+                Resource nextResource = getChildInternal(resource, childNameRaw);
+                if (nextResource != null) {
+
+                    resource = nextResource;
+                    resolutionPath.append("/").append(childNameRaw);
+
+                } else {
+
+                    String childName = null;
+                    ResourcePathIterator rpi = new ResourcePathIterator(
+                        childNameRaw);
+                    while (rpi.hasNext() && nextResource == null) {
+                        childName = rpi.next();
+                        nextResource = getChildInternal(resource, childName);
+                    }
+
+                    // switch the currentResource to the nextResource (may be
+                    // null)
+                    resource = nextResource;
+                    resolutionPath.append("/").append(childName);
+
+                    // terminate the search if a resource has been found
+                    // with the extension cut off
+                    if (nextResource != null) {
+                        break;
+                    }
+                }
+            }
+
+            // SLING-627: set the part cut off from the uriPath as
+            // sling.resolutionPathInfo property such that
+            // uriPath = curPath + sling.resolutionPathInfo
+            if (resource != null) {
+                final String path = resolutionPath.toString();
+                final String pathInfo = absPath.substring(path.length());
+
+                resource.getResourceMetadata().setResolutionPath(path);
+                resource.getResourceMetadata().setResolutionPathInfo(pathInfo);
+
+                LOGGER.debug(
+                    "resolveInternal: Found resource {} with path info {} for {}",
+                    new Object[] { resource, pathInfo, absPath });
+            }
+        }
+
+        return resource;
+    }
+
+    private Resource getChildInternal(Resource parent, String childName) {
+        Resource child = getResource(parent, childName);
+        if (child != null) {
+            String alias = getProperty(child, PROP_REDIRECT_INTERNAL);
+            if (alias != null) {
+                // TODO: might be a redirect ??
+                LOGGER.warn(
+                    "getChildInternal: Internal redirect to {} for Resource {} is not supported yet, ignoring",
+                    alias, child);
+            }
+
+            // we have the resource name, continue with the next level
+            return child;
+        }
+
+        // we do not have a child with the exact name, so we look for
+        // a child, whose alias matches the childName
+        Iterator<Resource> children = listChildren(parent);
+        while (children.hasNext()) {
+            child = children.next();
+            if (!child.getPath().endsWith(JCR_CONTENT_LEAF)){
+            	String[] aliases = getProperty(child, PROP_ALIAS, String[].class);
+            	if (aliases != null) {
+            		for (String alias : aliases) {
+            			if (childName.equals(alias)) {
+            				LOGGER.debug(
+            						"getChildInternal: Found Resource {} with alias {} to use",
+            						child, childName);
+            				return child;
+            			}
+            		}
+            	}
+            }
+        }
+
+        // no match for the childName found
+        LOGGER.debug("getChildInternal: Resource {} has no child {}", parent,
+            childName);
+        return null;
+    }
+
+    /**
+     * Creates a JcrNodeResource with the given path if existing
+     */
+    protected Resource getResourceInternal(String path) {
+
+        Resource resource = rootProvider.getResource(this, path);
+        if (resource != null) {
+            resource.getResourceMetadata().setResolutionPath(path);
+            return resource;
+        }
+
+        LOGGER.debug(
+            "getResourceInternal: Cannot resolve path '{}' to a resource", path);
+        return null;
+    }
+
+    public String getProperty(Resource res, String propName) {
+        return getProperty(res, propName, String.class);
+    }
+
+    public <Type> Type getProperty(Resource res, String propName,
+            Class<Type> type) {
+
+        // check the property in the resource itself
+        ValueMap props = res.adaptTo(ValueMap.class);
+        if (props != null) {
+            Type prop = props.get(propName, type);
+            if (prop != null) {
+                LOGGER.debug("getProperty: Resource {} has property {}={}",
+                    new Object[] { res, propName, prop });
+                return prop;
+            }
+            // otherwise, check it in the jcr:content child resource
+            // This is a special case checking for JCR based resources
+            // we directly use the deep resolution of properties of the
+            // JCR value map implementation - this does not work
+            // in non JCR environments, however in non JCR envs there
+            // is usually no "jcr:content" child node anyway
+            prop = props.get("jcr:content/" + propName, type);
+            return prop;
+        }
+
+        return null;
+    }
+
+    /**
+     * Returns the <code>path</code> as an absolute path. If the path is
+     * already absolute it is returned unmodified (the same instance actually).
+     * If the path is relative it is made absolute by prepending the first entry
+     * of the {@link #getSearchPath() search path}.
+     *
+     * @param path The path to ensure absolute
+     * @return The absolute path as explained above
+     */
+    private String ensureAbsPath(String path) {
+        if (!path.startsWith("/")) {
+            path = getSearchPath()[0] + path;
+        }
+        return path;
+    }
+
+    private String mangleNamespaces(String absPath) {
+        if (factory.isMangleNamespacePrefixes() && absPath.contains(MANGLE_NAMESPACE_OUT_SUFFIX)) {
+            Pattern p = Pattern.compile(MANGLE_NAMESPACE_OUT);
+            Matcher m = p.matcher(absPath);
+
+            StringBuffer buf = new StringBuffer();
+            while (m.find()) {
+                String replacement = MANGLE_NAMESPACE_IN_PREFIX + m.group(1) + MANGLE_NAMESPACE_IN_SUFFIX;
+                m.appendReplacement(buf, replacement);
+            }
+
+            m.appendTail(buf);
+
+            absPath = buf.toString();
+        }
+
+        return absPath;
+    }
+
+    private String unmangleNamespaces(String absPath) {
+        if (factory.isMangleNamespacePrefixes() && absPath.contains(MANGLE_NAMESPACE_IN_PREFIX)) {
+            Pattern p = Pattern.compile(MANGLE_NAMESPACE_IN);
+            Matcher m = p.matcher(absPath);
+            StringBuffer buf = new StringBuffer();
+            while (m.find()) {
+                String namespace = m.group(1);
+                try {
+
+                    // throws if "namespace" is not a registered
+                    // namespace prefix
+                    getSession().getNamespaceURI(namespace);
+
+                    String replacement = MANGLE_NAMESPACE_OUT_PREFIX
+                        + namespace + MANGLE_NAMESPACE_OUT_SUFFIX;
+                    m.appendReplacement(buf, replacement);
+
+                } catch (NamespaceException ne) {
+
+                    // not a valid prefix
+                    LOGGER.debug(
+                        "unmangleNamespaces: '{}' is not a prefix, not unmangling",
+                        namespace);
+
+                } catch (RepositoryException re) {
+
+                    LOGGER.warn(
+                        "unmangleNamespaces: Problem checking namespace '{}'",
+                        namespace, re);
+
+                }
+            }
+            m.appendTail(buf);
+            absPath = buf.toString();
+        }
+
+        return absPath;
+    }
+
+    private boolean isSupportedQueryLanguage(String language) {
+        try {
+            String[] supportedLanguages = adaptTo(Session.class).getWorkspace().
+                getQueryManager().getSupportedQueryLanguages();
+            for (String lang : supportedLanguages) {
+                if (lang.equals(language)) {
+                    return true;
+                }
+            }
+        } catch (RepositoryException e) {
+            LOGGER.error("Unable to discover supported query languages", e);
+        }
+        return false;
+    }
+}
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/console/ResourceResolverWebConsolePlugin.java b/src/main/java/org/apache/sling/resourceresolver/impl/console/ResourceResolverWebConsolePlugin.java
new file mode 100644
index 0000000..c1cbd64
--- /dev/null
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/console/ResourceResolverWebConsolePlugin.java
@@ -0,0 +1,385 @@
+/*
+ * 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.resource.internal;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.List;
+
+import javax.jcr.Session;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.request.ResponseUtil;
+import org.apache.sling.jcr.resource.internal.helper.MapEntries;
+import org.apache.sling.jcr.resource.internal.helper.MapEntry;
+import org.apache.sling.jcr.resource.internal.helper.URI;
+import org.apache.sling.jcr.resource.internal.helper.URIException;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceRegistration;
+
+public class JcrResourceResolverWebConsolePlugin extends
+        HttpServlet {
+
+    private static final long serialVersionUID = 0;
+
+    private static final String ATTR_TEST = "plugin.test";
+
+    private static final String ATTR_SUBMIT = "plugin.submit";
+
+    private static final String PAR_MSG = "msg";
+    private static final String PAR_TEST = "test";
+
+    private final transient JcrResourceResolverFactoryImpl resolverFactory;
+
+    private transient ServiceRegistration service;
+
+    JcrResourceResolverWebConsolePlugin(BundleContext context,
+            JcrResourceResolverFactoryImpl resolverFactory) {
+        this.resolverFactory = resolverFactory;
+
+        Dictionary<String, Object> props = new Hashtable<String, Object>();
+        props.put(Constants.SERVICE_DESCRIPTION,
+            "JCRResourceResolver Web Console Plugin");
+        props.put(Constants.SERVICE_VENDOR, "The Apache Software Foundation");
+        props.put(Constants.SERVICE_PID, getClass().getName());
+        props.put("felix.webconsole.label", "jcrresolver");
+        props.put("felix.webconsole.title", "Sling Resource Resolver");
+        props.put("felix.webconsole.configprinter.modes", "always");
+
+        service = context.registerService(new String[] {
+                "javax.servlet.Servlet" },
+            this, props);
+    }
+
+    void dispose() {
+        if (service != null) {
+            service.unregister();
+            service = null;
+        }
+    }
+
+    @Override
+    protected void doGet(final HttpServletRequest request, final HttpServletResponse response)
+    throws ServletException, IOException {
+        final String msg = request.getParameter(PAR_MSG);
+        final String test;
+        if ( msg != null ) {
+            test = request.getParameter(PAR_TEST);
+        } else {
+            test = null;
+        }
+
+        final PrintWriter pw = response.getWriter();
+
+        pw.println("<table class='content' cellpadding='0' cellspacing='0' width='100%'>");
+
+        final MapEntries mapEntries = resolverFactory.getMapEntries();
+
+        titleHtml(pw, "Configuration", null);
+        pw.println("<tr class='content'>");
+        pw.println("<td class='content'>Resource Search Path</td>");
+        pw.print("<td class='content' colspan='2'>");
+        pw.print(Arrays.asList(resolverFactory.getSearchPath()).toString());
+        pw.print("</td>");
+        pw.println("</tr>");
+        pw.println("<tr class='content'>");
+        pw.println("<td class='content'>Namespace Mangling</td>");
+        pw.print("<td class='content' colspan='2'>");
+        pw.print(resolverFactory.isMangleNamespacePrefixes() ? "Enabled" : "Disabled");
+        pw.print("</td>");
+        pw.println("</tr>");
+        pw.println("<tr class='content'>");
+        pw.println("<td class='content'>Mapping Location</td>");
+        pw.print("<td class='content' colspan='2'>");
+        pw.print(resolverFactory.getMapRoot());
+        pw.print("</td>");
+        pw.println("</tr>");
+
+        separatorHtml(pw);
+
+        titleHtml(
+            pw,
+            "Configuration Test",
+            "To test the configuration, enter an URL or a resource path into " +
+            "the field and click 'Resolve' to resolve the URL or click 'Map' " +
+            "to map the resource path. To simulate a map call that takes the " +
+            "current request into account, provide a full URL whose " +
+            "scheme/host/port prefix will then be used as the request " +
+            "information. The path passed to map will always be the path part " +
+            "of the URL.");
+
+        pw.println("<tr class='content'>");
+        pw.println("<td class='content'>Test</td>");
+        pw.print("<td class='content' colspan='2'>");
+        pw.print("<form method='post'>");
+        pw.print("<input type='text' name='" + ATTR_TEST + "' value='");
+        if ( test != null ) {
+            pw.print(ResponseUtil.escapeXml(test));
+        }
+        pw.println("' class='input' size='50'>");
+        pw.println("&nbsp;&nbsp;<input type='submit' name='" + ATTR_SUBMIT
+            + "' value='Resolve' class='submit'>");
+        pw.println("&nbsp;&nbsp;<input type='submit' name='" + ATTR_SUBMIT
+            + "' value='Map' class='submit'>");
+        pw.print("</form>");
+        pw.print("</td>");
+        pw.println("</tr>");
+
+        if (msg != null) {
+            pw.println("<tr class='content'>");
+            pw.println("<td class='content'>&nbsp;</td>");
+            pw.print("<td class='content' colspan='2'>");
+            pw.print(ResponseUtil.escapeXml(msg));
+            pw.println("</td>");
+            pw.println("</tr>");
+        }
+
+        separatorHtml(pw);
+
+        dumpMapHtml(
+            pw,
+            "Resolver Map Entries",
+            "Lists the entries used by the ResourceResolver.resolve methods to map URLs to Resources",
+            mapEntries.getResolveMaps());
+
+        separatorHtml(pw);
+
+        dumpMapHtml(
+            pw,
+            "Mapping Map Entries",
+            "Lists the entries used by the ResourceResolver.map methods to map Resource Paths to URLs",
+            mapEntries.getMapMaps());
+
+        pw.println("</table>");
+
+    }
+
+    @Override
+    protected void doPost(HttpServletRequest request,
+            HttpServletResponse response) throws ServletException, IOException {
+
+        final String test = request.getParameter(ATTR_TEST);
+        String msg = null;
+        if (test != null && test.length() > 0) {
+
+            Session session = null;
+            try {
+                // prepare the request for the resource resolver
+                HttpServletRequest helper = new ResolverRequest(request, test);
+
+                // get the resource resolver with an administrative session
+                session = resolverFactory.getRepository().loginAdministrative(
+                    null);
+                ResourceResolver resolver = resolverFactory.getResourceResolver(session);
+
+                // map or resolve as instructed
+                Object result;
+                if ("Map".equals(request.getParameter(ATTR_SUBMIT))) {
+                    if (helper.getServerName() == null) {
+                        result = resolver.map(helper.getPathInfo());
+                    } else {
+                        result = resolver.map(helper, helper.getPathInfo());
+                    }
+                } else {
+                    result = resolver.resolve(helper, helper.getPathInfo());
+                }
+
+                // set the result to render the result
+                msg = result.toString();
+
+            } catch (final Throwable t) {
+
+                // some error occurred, report it as a result
+                msg = "Test Failure: " + t;
+
+            } finally {
+                if (session != null) {
+                    session.logout();
+                }
+            }
+
+        }
+
+        // finally redirect
+        final String path = request.getContextPath() + request.getServletPath() + request.getPathInfo();
+        final String redirectTo;
+        if ( msg == null ) {
+            redirectTo = path;
+        } else {
+            redirectTo = path + '?' + PAR_MSG + '=' + encodeParam(msg) + '&' + PAR_TEST + '=' + encodeParam(test);
+        }
+        response.sendRedirect(redirectTo);
+    }
+
+    private String encodeParam(final String value) {
+        try {
+            return URLEncoder.encode(value, "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            // should never happen
+            return value;
+        }
+    }
+
+    // ---------- ConfigurationPrinter
+
+    public void printConfiguration(PrintWriter pw) {
+        final MapEntries mapEntries = resolverFactory.getMapEntries();
+
+        dumpMapText(
+            pw,
+            "Resolver Map Entries",
+            mapEntries.getResolveMaps());
+
+         separatorText(pw);
+
+        dumpMapText(
+            pw,
+            "Mapping Map Entries",
+            mapEntries.getMapMaps());
+    }
+
+    // ---------- internal
+
+    private void dumpMapHtml(PrintWriter pw, String title, String description,
+            Collection<MapEntry> list) {
+
+        titleHtml(pw, title, description);
+
+        pw.println("<tr class='content'>");
+        pw.println("<th class='content'>Pattern</th>");
+        pw.println("<th class='content'>Replacement</th>");
+        pw.println("<th class='content'>Redirect</th>");
+        pw.println("</tr>");
+
+        for (MapEntry entry : list) {
+            pw.println("<tr class='content'>");
+            pw.println("<td class='content' style='vertical-align: top'>"
+                + entry.getPattern() + "</td>");
+
+            pw.print("<td class='content' style='vertical-align: top'>");
+            String[] repls = entry.getRedirect();
+            for (String repl : repls) {
+                pw.print(repl + "<br/>");
+            }
+            pw.println("</td>");
+
+            pw.print("<td class='content' style='vertical-align: top'>");
+            if (entry.isInternal()) {
+                pw.print("internal");
+            } else {
+                pw.print("external: " + entry.getStatus());
+            }
+            pw.println("</td>");
+
+        }
+    }
+
+    private void titleHtml(PrintWriter pw, String title, String description) {
+        pw.println("<tr class='content'>");
+        pw.println("<th colspan='3'class='content container'>" + title
+            + "</th>");
+        pw.println("</tr>");
+
+        if (description != null) {
+            pw.println("<tr class='content'>");
+            pw.println("<td colspan='3'class='content'>" + description
+                + "</th>");
+            pw.println("</tr>");
+        }
+    }
+
+    private void separatorHtml(PrintWriter pw) {
+        pw.println("<tr class='content'>");
+        pw.println("<td class='content' colspan='3'>&nbsp;</td>");
+        pw.println("</tr>");
+    }
+
+    private void dumpMapText(PrintWriter pw, String title,
+            Collection<MapEntry> list) {
+
+        pw.println(title);
+
+        final String format = "%25s%25s%15s\r\n";
+        pw.printf(format, "Pattern", "Replacement", "Redirect");
+
+        for (MapEntry entry : list) {
+            final List<String> redir = Arrays.asList(entry.getRedirect());
+            final String status = entry.isInternal()
+                    ? "internal"
+                    : "external: " + entry.getStatus();
+            pw.printf(format, entry.getPattern(), redir, status);
+        }
+    }
+
+    private void separatorText(PrintWriter pw) {
+        pw.println();
+    }
+
+    private static class ResolverRequest extends HttpServletRequestWrapper {
+
+        private final URI uri;
+
+        public ResolverRequest(HttpServletRequest request, String uriString)
+                throws URIException {
+            super(request);
+            uri = new URI(uriString, false);
+        }
+
+        @Override
+        public String getScheme() {
+            return uri.getScheme();
+        }
+
+        @Override
+        public String getServerName() {
+            try {
+                return uri.getHost();
+            } catch (URIException ue) {
+                return null;
+            }
+        }
+
+        @Override
+        public int getServerPort() {
+            return uri.getPort();
+        }
+
+        @Override
+        public String getPathInfo() {
+            try {
+                return uri.getPath();
+            } catch (URIException ue) {
+                return "";
+            }
+        }
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/helper/RedirectResource.java b/src/main/java/org/apache/sling/resourceresolver/impl/helper/RedirectResource.java
new file mode 100644
index 0000000..f51768b
--- /dev/null
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/helper/RedirectResource.java
@@ -0,0 +1,72 @@
+/*
+ * 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.resource.internal.helper;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.sling.adapter.annotations.Adaptable;
+import org.apache.sling.adapter.annotations.Adapter;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.SyntheticResource;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.api.wrappers.ValueMapDecorator;
+
+@Adaptable(adaptableClass = Resource.class, adapters = @Adapter(value = { Map.class, ValueMap.class }))
+public final class RedirectResource extends SyntheticResource {
+
+    static final String RT_SLING_REDIRECT = "sling:redirect";
+
+    static final String PROP_SLING_TARGET = "sling:target";
+
+    static final String PROP_SLING_STATUS = "sling:status";
+
+    private final Map<String, Object> values;
+
+    public RedirectResource(final ResourceResolver resolver, final String path,
+            final String target, final int status) {
+        super(resolver, path, RT_SLING_REDIRECT);
+
+        HashMap<String, Object> props = new HashMap<String, Object>();
+        props.put(PROP_SLING_TARGET, target);
+        props.put(PROP_SLING_STATUS, status);
+        this.values = Collections.unmodifiableMap(props);
+    }
+
+    /**
+     * @see org.apache.sling.api.adapter.Adaptable#adaptTo(java.lang.Class)
+     */
+    @SuppressWarnings("unchecked")
+    public <AdapterType> AdapterType adaptTo(Class<AdapterType> type) {
+        if (type == ValueMap.class) {
+            return (AdapterType) new ValueMapDecorator(values);
+        } else if (type == Map.class) {
+            return (AdapterType) values;
+        }
+
+        return super.adaptTo(type);
+    }
+
+    @Override
+    public String toString() {
+        return super.toString() + ", values=" + values;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/helper/ResourceDecoratorTracker.java b/src/main/java/org/apache/sling/resourceresolver/impl/helper/ResourceDecoratorTracker.java
new file mode 100644
index 0000000..ffc6a31
--- /dev/null
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/helper/ResourceDecoratorTracker.java
@@ -0,0 +1,140 @@
+/*
+ * 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.resource.internal;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceDecorator;
+import org.apache.sling.commons.osgi.OsgiUtil;
+
+/**
+ * Helper class to track the resource decorators and keep
+ * them sorted by their service ranking.
+ */
+public class ResourceDecoratorTracker {
+
+    private static final ResourceDecorator[] EMPTY_ARRAY = new ResourceDecorator[0];
+
+    /**
+     * The (optional) resource decorators, working copy.
+     */
+    protected final List<ResourceDecoratorEntry> resourceDecorators = new ArrayList<ResourceDecoratorEntry>();
+
+    /**
+     * An array of the above, updates when changes are created.
+     */
+    private volatile ResourceDecorator[] resourceDecoratorsArray = EMPTY_ARRAY;
+
+    public void close() {
+        synchronized (this.resourceDecorators) {
+            this.resourceDecorators.clear();
+            this.resourceDecoratorsArray = EMPTY_ARRAY;
+        }
+    }
+
+    /** Decorate a resource.  */
+    public Resource decorate(final Resource resource, String workspaceName) {
+        Resource result = resource;
+        final ResourceDecorator[] decorators = this.resourceDecoratorsArray;
+        for(final ResourceDecorator decorator : decorators) {
+            final Resource original = result;
+            result = decorator.decorate(original);
+            if ( result == null ) {
+                result = original;
+            }
+        }
+        if (workspaceName != null) {
+            result = new WorkspaceDecoratedResource(result, workspaceName);
+        }
+        return result;
+    }
+
+    public ResourceDecorator[] getResourceDecorators() {
+        return this.resourceDecoratorsArray;
+    }
+
+    protected void bindResourceDecorator(final ResourceDecorator decorator, final Map<String, Object> props) {
+        synchronized (this.resourceDecorators) {
+            this.resourceDecorators.add(new ResourceDecoratorEntry(decorator, OsgiUtil.getComparableForServiceRanking(props)));
+            Collections.sort(this.resourceDecorators);
+            updateResourceDecoratorsArray();
+        }
+    }
+
+    protected void unbindResourceDecorator(final ResourceDecorator decorator, final Map<String, Object> props) {
+        synchronized (this.resourceDecorators) {
+            final Iterator<ResourceDecoratorEntry> i = this.resourceDecorators.iterator();
+            while (i.hasNext()) {
+                final ResourceDecoratorEntry current = i.next();
+                if (current.decorator == decorator) {
+                    i.remove();
+                    break;
+                }
+            }
+            updateResourceDecoratorsArray();
+        }
+    }
+
+    /**
+     * Updates the ResourceDecorators array, this method is not thread safe and should only be
+     * called from a synchronized block.
+     */
+    protected void updateResourceDecoratorsArray() {
+        final ResourceDecorator[] decorators;
+        if (this.resourceDecorators.size() > 0) {
+            decorators = new ResourceDecorator[this.resourceDecorators.size()];
+            int index = 0;
+            final Iterator<ResourceDecoratorEntry> i = this.resourceDecorators.iterator();
+            while (i.hasNext()) {
+                decorators[index] = i.next().decorator;
+                index++;
+            }
+        } else {
+            decorators = EMPTY_ARRAY;
+        }
+        this.resourceDecoratorsArray = decorators;
+    }
+
+    /**
+     * Internal class to keep track of the resource decorators.
+     */
+    private static final class ResourceDecoratorEntry implements Comparable<ResourceDecoratorEntry> {
+
+        final Comparable<Object> comparable;
+
+        final ResourceDecorator decorator;
+
+        public ResourceDecoratorEntry(final ResourceDecorator d,
+                final Comparable<Object> comparable) {
+            this.comparable = comparable;
+            this.decorator = d;
+        }
+
+        public int compareTo(ResourceDecoratorEntry o) {
+            return comparable.compareTo(o.comparable);
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/helper/ResourceIterator.java b/src/main/java/org/apache/sling/resourceresolver/impl/helper/ResourceIterator.java
new file mode 100644
index 0000000..47eed58
--- /dev/null
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/helper/ResourceIterator.java
@@ -0,0 +1,294 @@
+/*
+ * 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.resource.internal.helper;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceProvider;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.SyntheticResource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The <code>ResourceIterator</code> implements the
+ * <code>Iterator&lt;Resource&gt;</code> returned from the
+ * <code>ResourceResolver.listChidlren(Resource)</code> method.
+ * <p>
+ * Note: This iterator is created by the
+ * <code>JcrResourceResolver.listChildren(Resource)</code> and is not intended
+ * for general use by any other code. This class uses internal API of the
+ * {@link ResourceProviderEntry} class.
+ */
+public class ResourceIterator implements Iterator<Resource> {
+
+    /** default log */
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    /**
+     * The resource whose children are listed
+     */
+    private final Resource parentResource;
+
+    /**
+     * The root {@link ResourceProviderEntry} used to walk down the resource
+     * tree to collect entries which might provide children for the
+     * {@link #parentResource}.
+     */
+    private final ResourceProviderEntry rootProviderEntry;
+
+    /**
+     * <code>ResourceProvider</code> objects registered as nodes above the
+     * {@link #parentResource} up to the root of the resource tree
+     */
+    private final Iterator<ResourceProvider> providers;
+
+    /**
+     * The child {@link ResourceProviderEntry} registered at the node of the
+     * {@link #parentResource} in the resource tree. This may be
+     * <code>null</code> if there is no provider entry registered at that
+     * location and will be set to <code>null</code> once all entries have been
+     * processed.
+     */
+    private Iterator<ResourceProviderEntry> baseEntryValues;
+
+    /**
+     * An iterator of child resources provided by the current provider entry of
+     * the {@link #providers} iterator.
+     */
+    private Iterator<Resource> resources;
+
+    /**
+     * The next resource to be returned from the {@link #next()} method. If this
+     * is <code>null</code> the {@link #hasNext()} returns <code>false</code>.
+     */
+    private Resource nextResource;
+
+    /**
+     * Map of synthetic resources returned from resource providers while
+     * scanning for children of the {@link #parentResource}. These delayed
+     * entries are returned after all non-synthetic resources have been
+     * returned. Any delayed entry whose path matches the path of a
+     * non-synthetic resource will not returned.
+     */
+    private Map<String, Resource> delayed;
+
+    /**
+     * Set of paths of resources already returned. This is used to prevent
+     * duplicate return of resources.
+     */
+    private Set<String> visited;
+
+    /**
+     * The absolute path prefix of the {@link #parentResource} resource with a
+     * trailing slash to build the absolute path of child resources.
+     */
+    private String iteratorPath;
+
+    /**
+     * Iterator on the map of {@link #delayed} synthetic resources
+     */
+    private Iterator<Resource> delayedIter;
+
+    public ResourceIterator(final Resource parentResource,
+            final ResourceProviderEntry rootProviderEntry) {
+        this.parentResource = parentResource;
+        this.rootProviderEntry = rootProviderEntry;
+
+        log.debug("Child Iterator for {}", parentResource.getPath());
+
+        String path = parentResource.getPath();
+        if (!path.endsWith("/")) {
+            path += "/";
+        }
+
+        // gather the providers in linked set, such that we keep
+        // the order of addition and make sure we only get one entry
+        // for each resource provider
+        Set<ResourceProvider> providersSet = new LinkedHashSet<ResourceProvider>();
+        ResourceProviderEntry atPath = getResourceProviders(path, providersSet);
+
+        if (log.isDebugEnabled()) {
+            log.debug(" Provider Set for path {} {} ", path,
+                Arrays.toString(providersSet.toArray(new ResourceProvider[0])));
+        }
+        this.iteratorPath = path;
+        providers = providersSet.iterator();
+        baseEntryValues = (atPath != null) ? atPath.values().iterator() : null;
+        delayed = new HashMap<String, Resource>();
+        visited = new HashSet<String>();
+        nextResource = seek();
+    }
+
+    public boolean hasNext() {
+        return nextResource != null;
+    }
+
+    public Resource next() {
+        if (!hasNext()) {
+            throw new NoSuchElementException();
+        }
+
+        Resource result = nextResource;
+        nextResource = seek();
+        log.debug("  Child resource [{}] [{}] ", iteratorPath, result.getPath());
+        return result;
+    }
+
+    public void remove() {
+        throw new UnsupportedOperationException("remove");
+    }
+
+    private Resource seek() {
+        while (delayedIter == null) {
+            while ((resources == null || !resources.hasNext())
+                && providers.hasNext()) {
+                ResourceProvider provider = providers.next();
+                resources = provider.listChildren(parentResource);
+                log.debug("     Checking Provider {} ", provider);
+            }
+
+            if (resources != null && resources.hasNext()) {
+                Resource res = resources.next();
+                String resPath = res.getPath();
+
+                if (visited.contains(resPath)) {
+
+                    // ignore a path, we have already visited and
+                    // ensure it will not be listed as a delayed
+                    // resource at the end
+                    delayed.remove(resPath);
+
+                } else if (res instanceof SyntheticResource) {
+
+                    // don't return synthetic resources right away,
+                    // since a concrete resource for the same path
+                    // may be provided later on
+                    delayed.put(resPath, res);
+
+                } else {
+
+                    // we use this concrete, unvisited resource but
+                    // mark it as visited and remove from delayed
+                    visited.add(resPath);
+                    delayed.remove(resPath);
+                    log.debug("      resource {} {}", resPath, res.getClass());
+                    return res;
+
+                }
+
+            } else if (baseEntryValues != null) {
+
+                while (baseEntryValues.hasNext()) {
+                    final ResourceProviderEntry rpw = baseEntryValues.next();
+                    final String resPath = iteratorPath + rpw.getPath();
+                    if (!visited.contains(resPath)) {
+                        final ResourceResolver rr = parentResource.getResourceResolver();
+                        final Resource res = rpw.getResourceFromProviders(rr,
+                            resPath);
+                        if (res == null) {
+                            if (!delayed.containsKey(resPath)) {
+                                delayed.put(resPath, new SyntheticResource(rr,
+                                    resPath,
+                                    ResourceProvider.RESOURCE_TYPE_SYNTHETIC));
+                            }
+                        } else {
+                            // return the real resource immediately, add
+                            // to the visited keys and ensure delayed
+                            // does not contain it
+                            delayed.remove(resPath);
+                            visited.add(resPath);
+                            log.debug("   B  resource {} {}", resPath,
+                                res.getClass());
+                            return res;
+                        }
+                    }
+                }
+
+                baseEntryValues = null;
+
+            } else {
+
+                // all resource providers and baseEntryValues have
+                // exhausted, so we should continue returning the
+                // delayed (synthetic resources)
+                delayedIter = delayed.values().iterator();
+            }
+        }
+
+        // we exhausted all resource providers with their concrete
+        // resources. now lets do the delayed (synthetic) resources
+        Resource res = delayedIter.hasNext() ? delayedIter.next() : null;
+        if (res != null) {
+            log.debug("   D  resource {} {}", res.getPath(), res.getClass());
+        }
+        return res;
+    }
+
+    /**
+     * Returns all resource providers which provider resources whose prefix is
+     * the given path.
+     *
+     * @param path The prefix path to match the resource provider roots against
+     * @param providers The set of already found resource providers to which any
+     *            additional resource providers are added.
+     * @return The ResourceProviderEntry at the node identified with the path or
+     *         <code>null</code> if there is no entry at the given location
+     */
+    private ResourceProviderEntry getResourceProviders(String path,
+            Set<ResourceProvider> providers) {
+
+        // collect providers along the ancestor path segements
+        String[] elements = ResourceProviderEntry.split(path, '/');
+        ResourceProviderEntry base = rootProviderEntry;
+        for (String element : elements) {
+            if (base.containsKey(element)) {
+                base = base.get(element);
+                if (log.isDebugEnabled()) {
+                    log.debug("Loading from {}  {} ", element,
+                        base.getResourceProviders().length);
+                }
+                for (ResourceProvider rp : base.getResourceProviders()) {
+                    log.debug("Adding {} for {} ", rp, path);
+                    providers.add(rp);
+                }
+            } else {
+                log.debug("No container for {} ", element);
+                base = null;
+                break;
+            }
+        }
+
+        // add in providers at this node in the tree, ie the root provider
+        for (ResourceProvider rp : rootProviderEntry.getResourceProviders()) {
+            log.debug("Loading All at {} ", path);
+            providers.add(rp);
+        }
+        return base;
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/helper/ResourceIteratorDecorator.java b/src/main/java/org/apache/sling/resourceresolver/impl/helper/ResourceIteratorDecorator.java
new file mode 100644
index 0000000..0f67495
--- /dev/null
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/helper/ResourceIteratorDecorator.java
@@ -0,0 +1,55 @@
+/*
+ * 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.resource.internal;
+
+import java.util.Iterator;
+
+import org.apache.sling.api.resource.Resource;
+
+/**
+ * Resource iterator handling the decoration of resources.
+ */
+public class ResourceIteratorDecorator implements Iterator<Resource> {
+
+    private final ResourceDecoratorTracker tracker;
+
+    private final String workspaceName;
+
+    private final Iterator<Resource> iterator;
+
+    public ResourceIteratorDecorator(final ResourceDecoratorTracker tracker,
+            final String workspaceName,
+            final Iterator<Resource> iterator) {
+        this.tracker = tracker;
+        this.iterator = iterator;
+        this.workspaceName = workspaceName;
+    }
+
+    public boolean hasNext() {
+        return this.iterator.hasNext();
+    }
+
+    public Resource next() {
+        return this.tracker.decorate(this.iterator.next(), workspaceName);
+    }
+
+    public void remove() {
+        this.iterator.remove();
+    }
+}
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/helper/ResourcePathIterator.java b/src/main/java/org/apache/sling/resourceresolver/impl/helper/ResourcePathIterator.java
new file mode 100644
index 0000000..c6cc059
--- /dev/null
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/helper/ResourcePathIterator.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.resource.internal.helper;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * Iterate over the the HTTP request path by creating shorter segments of that
+ * path using "." as a separator.
+ * <p>
+ * For example, if path = /some/stuff.a4.html/xyz.ext the sequence is:
+ * <ol>
+ * <li> /some/stuff.a4.html/xyz.ext </li>
+ * <li> /some/stuff.a4.html/xyz </li>
+ * <li> /some/stuff.a4</li>
+ * <li> /some/stuff </li>
+ * </ol>
+ * <p>
+ * The root path (/) is never returned.
+ */
+public class ResourcePathIterator implements Iterator<String> {
+
+    // the next path to return, null if nothing more to return
+    private String nextPath;
+
+    /**
+     * Creates a new instance iterating over the given path
+     *
+     * @param path The path to iterate over. If this is empty or
+     *            <code>null</code> this iterator will not return anything.
+     */
+    public ResourcePathIterator(String path) {
+
+        if (path == null || path.length() == 0) {
+
+            // null or empty path, there is nothing to return
+            nextPath = null;
+
+        } else {
+
+            // find last non-slash character
+            int i = path.length() - 1;
+            while (i >= 0 && path.charAt(i) == '/') {
+                i--;
+            }
+
+            if (i < 0) {
+                // only slashes, assume root node
+                nextPath = "/";
+
+            } else if (i < path.length() - 1) {
+                // cut off slash
+                nextPath = path.substring(0, i + 1);
+
+            } else {
+                // no trailing slash
+                nextPath = path;
+            }
+        }
+    }
+
+    public boolean hasNext() {
+        return nextPath != null;
+    }
+
+    public String next() {
+        if (!hasNext()) {
+            throw new NoSuchElementException();
+        }
+
+        final String result = nextPath;
+
+        // find next path
+        int lastDot = nextPath.lastIndexOf('.');
+        nextPath = (lastDot > 0) ? nextPath.substring(0, lastDot) : null;
+
+        return result;
+    }
+
+    public void remove() {
+        throw new UnsupportedOperationException("remove");
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/helper/StarResource.java b/src/main/java/org/apache/sling/resourceresolver/impl/helper/StarResource.java
new file mode 100644
index 0000000..e617b1e
--- /dev/null
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/helper/StarResource.java
@@ -0,0 +1,109 @@
+/*
+ * 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.resource.internal.helper.starresource;
+
+import java.util.Map;
+
+import org.apache.sling.adapter.annotations.Adaptable;
+import org.apache.sling.adapter.annotations.Adapter;
+import org.apache.sling.api.SlingException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceMetadata;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.api.resource.SyntheticResource;
+import org.apache.sling.api.resource.ValueMap;
+
+/** Used to provide the equivalent of an empty Node for GET requests
+ *  to *.something (SLING-344)
+ */
+@Adaptable(adaptableClass = Resource.class, adapters = @Adapter(value = { String.class }))
+public class StarResource extends SyntheticResource {
+
+    final static String SLASH_STAR = "/*";
+    public final static String DEFAULT_RESOURCE_TYPE = "sling:syntheticStarResource";
+
+    private static final String UNSET_RESOURCE_SUPER_TYPE = "<unset>";
+
+    private String resourceSuperType;
+
+    @SuppressWarnings("serial")
+    static class SyntheticStarResourceException extends SlingException {
+        SyntheticStarResourceException(String reason, Throwable cause) {
+            super(reason, cause);
+        }
+    }
+
+    /** True if a StarResource should be used for the given request, if
+     *  a real Resource was not found */
+    public static boolean appliesTo(String path) {
+        return path.contains(SLASH_STAR) || path.endsWith(SLASH_STAR);
+    }
+
+    /**
+     * Returns true if the path of the resource ends with the
+     * {@link #SLASH_STAR} and therefore should be considered a star
+     * resource.
+     */
+    public static boolean isStarResource(Resource res) {
+        return res.getPath().endsWith(SLASH_STAR);
+    }
+
+    public StarResource(ResourceResolver resourceResolver, String path) {
+        super(resourceResolver, getResourceMetadata(path), DEFAULT_RESOURCE_TYPE);
+        resourceSuperType = UNSET_RESOURCE_SUPER_TYPE;
+    }
+
+    /**
+     * Calls {@link ResourceUtil#getResourceSuperType(ResourceResolver, String)} method
+     * to dynamically resolve the resource super type of this star resource.
+     */
+    public String getResourceSuperType() {
+        // Yes, this isn't how you're supposed to compare Strings, but this is intentional.
+        if (resourceSuperType == UNSET_RESOURCE_SUPER_TYPE) {
+            resourceSuperType = ResourceUtil.getResourceSuperType(this.getResourceResolver(),
+                    this.getResourceType());
+        }
+        return resourceSuperType;
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <Type> Type adaptTo(Class<Type> type) {
+        if ( type == String.class ) {
+            return (Type)"";
+        }
+        return super.adaptTo(type);
+    }
+
+    /** Get our ResourceMetadata for given path */
+    static ResourceMetadata getResourceMetadata(String path) {
+    	ResourceMetadata result = new ResourceMetadata();
+
+    	// The path is up to /*, what follows is pathInfo
+        final int index = path.indexOf(SLASH_STAR);
+        if(index >= 0) {
+            result.setResolutionPath(path.substring(0, index) + SLASH_STAR);
+            result.setResolutionPathInfo(path.substring(index + SLASH_STAR.length()));
+        } else {
+            result.setResolutionPath(path);
+        }
+        return result;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/helper/URI.java b/src/main/java/org/apache/sling/resourceresolver/impl/helper/URI.java
new file mode 100644
index 0000000..7bdbb8d
--- /dev/null
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/helper/URI.java
@@ -0,0 +1,4200 @@
+/*
+ * 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.resource.internal.helper;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.io.UnsupportedEncodingException;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.HashMap;
+import java.util.Locale;
+
+import org.apache.sling.api.SlingException;
+
+/**
+ * The interface for the URI(Uniform Resource Identifiers) version of RFC 2396.
+ * This class has the purpose of supportting of parsing a URI reference to
+ * extend any specific protocols, the character encoding of the protocol to be
+ * transported and the charset of the document.
+ * <p>
+ * A URI is always in an "escaped" form, since escaping or unescaping a
+ * completed URI might change its semantics.
+ * <p>
+ * Implementers should be careful not to escape or unescape the same string more
+ * than once, since unescaping an already unescaped string might lead to
+ * misinterpreting a percent data character as another escaped character, or
+ * vice versa in the case of escaping an already escaped string.
+ * <p>
+ * In order to avoid these problems, data types used as follows:
+ * <p>
+ * <blockquote>
+ *
+ * <pre>
+ *   URI character sequence: char
+ *   octet sequence: byte
+ *   original character sequence: String
+ * </pre>
+ *
+ * </blockquote>
+ * <p>
+ * So, a URI is a sequence of characters as an array of a char type, which is
+ * not always represented as a sequence of octets as an array of byte.
+ * <p>
+ * URI Syntactic Components
+ * <p>
+ * <blockquote>
+ *
+ * <pre>
+ * - In general, written as follows:
+ *   Absolute URI = &lt;scheme&gt:&lt;scheme-specific-part&gt;
+ *   Generic URI = &lt;scheme&gt;://&lt;authority&gt;&lt;path&gt;?&lt;query&gt;
+ * - Syntax
+ *   absoluteURI   = scheme ":" ( hier_part | opaque_part )
+ *   hier_part     = ( net_path | abs_path ) [ "?" query ]
+ *   net_path      = "//" authority [ abs_path ]
+ *   abs_path      = "/"  path_segments
+ * </pre>
+ *
+ * </blockquote>
+ * <p>
+ * The following examples illustrate URI that are in common use.
+ *
+ * <pre>
+ * ftp://ftp.is.co.za/rfc/rfc1808.txt
+ *    -- ftp scheme for File Transfer Protocol services
+ * gopher://spinaltap.micro.umn.edu/00/Weather/California/Los%20Angeles
+ *    -- gopher scheme for Gopher and Gopher+ Protocol services
+ * http://www.math.uio.no/faq/compression-faq/part1.html
+ *    -- http scheme for Hypertext Transfer Protocol services
+ * mailto:mduerst@ifi.unizh.ch
+ *    -- mailto scheme for electronic mail addresses
+ * news:comp.infosystems.www.servers.unix
+ *    -- news scheme for USENET news groups and articles
+ * telnet://melvyl.ucop.edu/
+ *    -- telnet scheme for interactive services via the TELNET Protocol
+ * </pre>
+ *
+ * Please, notice that there are many modifications from URL(RFC 1738) and
+ * relative URL(RFC 1808).
+ * <p>
+ * <b>The expressions for a URI</b>
+ * <p>
+ *
+ * <pre>
+ * For escaped URI forms
+ *  - URI(char[]) // constructor
+ *  - char[] getRawXxx() // method
+ *  - String getEscapedXxx() // method
+ *  - String toString() // method
+ * <p>
+ * For unescaped URI forms
+ *  - URI(String) // constructor
+ *  - String getXXX() // method
+ * </pre>
+ * <p>
+ * This class is a slightly modified version of the URI class distributed with
+ * Http Client 3.1. The changes involve removing dependencies to other Http
+ * Client classes and the Commons Codec library. To this avail the following
+ * methods have been added to this class:
+ * <ul>
+ * <li>getBytes, getAsciiString, getString, getAsciiBytes has been copied from
+ * the Http Client 3.1 EncodingUtils class.</li>
+ * <li>encodeUrl and decodeUrl have been copied from the Commons Codec URLCodec
+ * class.</li>
+ * </ul>
+ * The signatures have been simplified and adapted to the use in this class.
+ * Also the exception thrown has been changed to be {@link URIException}.
+ */
+public class URI implements Cloneable, Comparable<URI>, Serializable {
+
+    // ----------------------------------------------------------- Constructors
+
+    /** Create an instance as an internal use */
+    protected URI() {
+    }
+
+    /**
+     * Construct a URI from a string with the given charset. The input string
+     * can be either in escaped or unescaped form.
+     *
+     * @param s URI character sequence
+     * @param escaped <tt>true</tt> if URI character sequence is in escaped
+     *            form. <tt>false</tt> otherwise.
+     * @param charset the charset string to do escape encoding, if required
+     * @throws URIException If the URI cannot be created.
+     * @throws NullPointerException if input string is <code>null</code>
+     * @see #getProtocolCharset
+     * @since 3.0
+     */
+    public URI(String s, boolean escaped, String charset) throws URIException,
+            NullPointerException {
+        protocolCharset = charset;
+        parseUriReference(s, escaped);
+    }
+
+    /**
+     * Construct a URI from a string with the given charset. The input string
+     * can be either in escaped or unescaped form.
+     *
+     * @param s URI character sequence
+     * @param escaped <tt>true</tt> if URI character sequence is in escaped
+     *            form. <tt>false</tt> otherwise.
+     * @throws URIException If the URI cannot be created.
+     * @throws NullPointerException if input string is <code>null</code>
+     * @see #getProtocolCharset
+     * @since 3.0
+     */
+    public URI(String s, boolean escaped) throws URIException,
+            NullPointerException {
+        parseUriReference(s, escaped);
+    }
+
+    /**
+     * Construct a general URI from the given components.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     *   URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ]
+     *   absoluteURI   = scheme ":" ( hier_part | opaque_part )
+     *   opaque_part   = uric_no_slash *uric
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     * It's for absolute URI = &lt;scheme&gt;:&lt;scheme-specific-part&gt;#
+     * &lt;fragment&gt;.
+     *
+     * @param scheme the scheme string
+     * @param schemeSpecificPart scheme_specific_part
+     * @param fragment the fragment string
+     * @throws URIException If the URI cannot be created.
+     * @see #getDefaultProtocolCharset
+     */
+    public URI(String scheme, String schemeSpecificPart, String fragment)
+            throws URIException {
+
+        // validate and contruct the URI character sequence
+        if (scheme == null) {
+            throw new URIException(URIException.PARSING, "scheme required");
+        }
+        char[] s = scheme.toLowerCase().toCharArray();
+        if (validate(s, URI.scheme)) {
+            _scheme = s; // is_absoluteURI
+        } else {
+            throw new URIException(URIException.PARSING, "incorrect scheme");
+        }
+        _opaque = encode(schemeSpecificPart, allowed_opaque_part,
+            getProtocolCharset());
+        // Set flag
+        _is_opaque_part = true;
+        _fragment = fragment == null ? null : fragment.toCharArray();
+        setURI();
+    }
+
+    /**
+     * Construct a general URI from the given components.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     *   URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ]
+     *   absoluteURI   = scheme ":" ( hier_part | opaque_part )
+     *   relativeURI   = ( net_path | abs_path | rel_path ) [ "?" query ]
+     *   hier_part     = ( net_path | abs_path ) [ "?" query ]
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     * It's for absolute URI = &lt;scheme&gt;:&lt;path&gt;?&lt;query&gt;#&lt;
+     * fragment&gt; and relative URI = &lt;path&gt;?&lt;query&gt;#&lt;fragment
+     * &gt;.
+     *
+     * @param scheme the scheme string
+     * @param authority the authority string
+     * @param path the path string
+     * @param query the query string
+     * @param fragment the fragment string
+     * @throws URIException If the new URI cannot be created.
+     * @see #getDefaultProtocolCharset
+     */
+    public URI(String scheme, String authority, String path, String query,
+            String fragment) throws URIException {
+
+        // validate and contruct the URI character sequence
+        StringBuilder buff = new StringBuilder();
+        if (scheme != null) {
+            buff.append(scheme);
+            buff.append(':');
+        }
+        if (authority != null) {
+            buff.append("//");
+            buff.append(authority);
+        }
+        if (path != null) { // accept empty path
+            if ((scheme != null || authority != null) && !path.startsWith("/")) {
+                throw new URIException(URIException.PARSING,
+                    "abs_path requested");
+            }
+            buff.append(path);
+        }
+        if (query != null) {
+            buff.append('?');
+            buff.append(query);
+        }
+        if (fragment != null) {
+            buff.append('#');
+            buff.append(fragment);
+        }
+        parseUriReference(buff.toString(), false);
+    }
+
+    /**
+     * Construct a general URI from the given components.
+     *
+     * @param scheme the scheme string
+     * @param userinfo the userinfo string
+     * @param host the host string
+     * @param port the port number
+     * @throws URIException If the new URI cannot be created.
+     * @see #getDefaultProtocolCharset
+     */
+    public URI(String scheme, String userinfo, String host, int port)
+            throws URIException {
+
+        this(scheme, userinfo, host, port, null, null, null);
+    }
+
+    /**
+     * Construct a general URI from the given components.
+     *
+     * @param scheme the scheme string
+     * @param userinfo the userinfo string
+     * @param host the host string
+     * @param port the port number
+     * @param path the path string
+     * @throws URIException If the new URI cannot be created.
+     * @see #getDefaultProtocolCharset
+     */
+    public URI(String scheme, String userinfo, String host, int port,
+            String path) throws URIException {
+
+        this(scheme, userinfo, host, port, path, null, null);
+    }
+
+    /**
+     * Construct a general URI from the given components.
+     *
+     * @param scheme the scheme string
+     * @param userinfo the userinfo string
+     * @param host the host string
+     * @param port the port number
+     * @param path the path string
+     * @param query the query string
+     * @throws URIException If the new URI cannot be created.
+     * @see #getDefaultProtocolCharset
+     */
+    public URI(String scheme, String userinfo, String host, int port,
+            String path, String query) throws URIException {
+
+        this(scheme, userinfo, host, port, path, query, null);
+    }
+
+    /**
+     * Construct a general URI from the given components.
+     *
+     * @param scheme the scheme string
+     * @param userinfo the userinfo string
+     * @param host the host string
+     * @param port the port number
+     * @param path the path string
+     * @param query the query string
+     * @param fragment the fragment string
+     * @throws URIException If the new URI cannot be created.
+     * @see #getDefaultProtocolCharset
+     */
+    public URI(String scheme, String userinfo, String host, int port,
+            String path, String query, String fragment) throws URIException {
+
+        this(scheme, (host == null) ? null : ((userinfo != null)
+                ? userinfo + '@'
+                : "")
+            + host + ((port != -1) ? ":" + port : ""), path, query, fragment);
+    }
+
+    /**
+     * Construct a general URI from the given components.
+     *
+     * @param scheme the scheme string
+     * @param host the host string
+     * @param path the path string
+     * @param fragment the fragment string
+     * @throws URIException If the new URI cannot be created.
+     * @see #getDefaultProtocolCharset
+     */
+    public URI(String scheme, String host, String path, String fragment)
+            throws URIException {
+
+        this(scheme, host, path, null, fragment);
+    }
+
+    /**
+     * Construct a general URI with the given relative URI string.
+     *
+     * @param base the base URI
+     * @param relative the relative URI string
+     * @param escaped <tt>true</tt> if URI character sequence is in escaped
+     *            form. <tt>false</tt> otherwise.
+     * @throws URIException If the new URI cannot be created.
+     * @since 3.0
+     */
+    public URI(URI base, String relative, boolean escaped) throws URIException {
+        this(base, new URI(relative, escaped));
+    }
+
+    /**
+     * Construct a general URI with the given relative URI.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     *   URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ]
+     *   relativeURI   = ( net_path | abs_path | rel_path ) [ "?" query ]
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     * Resolving Relative References to Absolute Form. <strong>Examples of
+     * Resolving Relative URI References</strong> Within an object with a
+     * well-defined base URI of
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     *   http://a/b/c/d;p?q
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     * the relative URI would be resolved as follows: Normal Examples
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     *   g:h           =  g:h
+     *   g             =  http://a/b/c/g
+     *   ./g           =  http://a/b/c/g
+     *   g/            =  http://a/b/c/g/
+     *   /g            =  http://a/g
+     *   //g           =  http://g
+     *   ?y            =  http://a/b/c/?y
+     *   g?y           =  http://a/b/c/g?y
+     *   #s            =  (current document)#s
+     *   g#s           =  http://a/b/c/g#s
+     *   g?y#s         =  http://a/b/c/g?y#s
+     *   ;x            =  http://a/b/c/;x
+     *   g;x           =  http://a/b/c/g;x
+     *   g;x?y#s       =  http://a/b/c/g;x?y#s
+     *   .             =  http://a/b/c/
+     *   ./            =  http://a/b/c/
+     *   ..            =  http://a/b/
+     *   ../           =  http://a/b/
+     *   ../g          =  http://a/b/g
+     *   ../..         =  http://a/
+     *   ../../        =  http://a/
+     *   ../../g       =  http://a/g
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     * Some URI schemes do not allow a hierarchical syntax matching the
+     * <hier_part> syntax, and thus cannot use relative references.
+     *
+     * @param base the base URI
+     * @param relative the relative URI
+     * @throws URIException If the new URI cannot be created.
+     */
+    public URI(URI base, URI relative) throws URIException {
+
+        if (base._scheme == null) {
+            throw new URIException(URIException.PARSING, "base URI required");
+        }
+        if (base._scheme != null) {
+            this._scheme = base._scheme;
+            this._authority = base._authority;
+            this._is_net_path = base._is_net_path;
+        }
+        if (base._is_opaque_part || relative._is_opaque_part) {
+            this._scheme = base._scheme;
+            this._is_opaque_part = base._is_opaque_part
+                || relative._is_opaque_part;
+            this._opaque = relative._opaque;
+            this._fragment = relative._fragment;
+            this.setURI();
+            return;
+        }
+        boolean schemesEqual = Arrays.equals(base._scheme, relative._scheme);
+        if (relative._scheme != null
+            && (!schemesEqual || relative._authority != null)) {
+            this._scheme = relative._scheme;
+            this._is_net_path = relative._is_net_path;
+            this._authority = relative._authority;
+            if (relative._is_server) {
+                this._is_server = relative._is_server;
+                this._userinfo = relative._userinfo;
+                this._host = relative._host;
+                this._port = relative._port;
+            } else if (relative._is_reg_name) {
+                this._is_reg_name = relative._is_reg_name;
+            }
+            this._is_abs_path = relative._is_abs_path;
+            this._is_rel_path = relative._is_rel_path;
+            this._path = relative._path;
+        } else if (base._authority != null && relative._scheme == null) {
+            this._is_net_path = base._is_net_path;
+            this._authority = base._authority;
+            if (base._is_server) {
+                this._is_server = base._is_server;
+                this._userinfo = base._userinfo;
+                this._host = base._host;
+                this._port = base._port;
+            } else if (base._is_reg_name) {
+                this._is_reg_name = base._is_reg_name;
+            }
+        }
+        if (relative._authority != null) {
+            this._is_net_path = relative._is_net_path;
+            this._authority = relative._authority;
+            if (relative._is_server) {
+                this._is_server = relative._is_server;
+                this._userinfo = relative._userinfo;
+                this._host = relative._host;
+                this._port = relative._port;
+            } else if (relative._is_reg_name) {
+                this._is_reg_name = relative._is_reg_name;
+            }
+            this._is_abs_path = relative._is_abs_path;
+            this._is_rel_path = relative._is_rel_path;
+            this._path = relative._path;
+        }
+        // resolve the path and query if necessary
+        if (relative._authority == null
+            && (relative._scheme == null || schemesEqual)) {
+            if ((relative._path == null || relative._path.length == 0)
+                && relative._query == null) {
+                // handle a reference to the current document, see RFC 2396
+                // section 5.2 step 2
+                this._path = base._path;
+                this._query = base._query;
+            } else {
+                this._path = resolvePath(base._path, relative._path);
+            }
+        }
+        // base._query removed
+        if (relative._query != null) {
+            this._query = relative._query;
+        }
+        // base._fragment removed
+        if (relative._fragment != null) {
+            this._fragment = relative._fragment;
+        }
+        this.setURI();
+        // reparse the newly built URI, this will ensure that all flags are set
+        // correctly.
+        // TODO there must be a better way to do this
+        parseUriReference(new String(_uri), true);
+    }
+
+    // --------------------------------------------------- Instance Variables
+
+    /** Version ID for serialization */
+    static final long serialVersionUID = 604752400577948726L;
+
+    /**
+     * Cache the hash code for this URI.
+     */
+    protected int hash = 0;
+
+    /**
+     * This Uniform Resource Identifier (URI). The URI is always in an "escaped"
+     * form, since escaping or unescaping a completed URI might change its
+     * semantics.
+     */
+    protected char[] _uri = null;
+
+    /**
+     * The charset of the protocol used by this URI instance.
+     */
+    protected String protocolCharset = null;
+
+    /**
+     * The default charset of the protocol. RFC 2277, 2396
+     */
+    protected static String defaultProtocolCharset = "UTF-8";
+
+    /**
+     * The default charset of the document. RFC 2277, 2396 The platform's
+     * charset is used for the document by default.
+     */
+    protected static String defaultDocumentCharset = null;
+
+    protected static String defaultDocumentCharsetByLocale = null;
+
+    protected static String defaultDocumentCharsetByPlatform = null;
+    // Static initializer for defaultDocumentCharset
+    static {
+        Locale locale = Locale.getDefault();
+        // in order to support backward compatiblity
+        if (locale != null) {
+            defaultDocumentCharsetByLocale = LocaleToCharsetMap.getCharset(locale);
+            // set the default document charset
+            defaultDocumentCharset = defaultDocumentCharsetByLocale;
+        }
+        // in order to support platform encoding
+        try {
+            defaultDocumentCharsetByPlatform = System.getProperty("file.encoding");
+        } catch (SecurityException ignore) {
+        }
+        if (defaultDocumentCharset == null) {
+            // set the default document charset
+            defaultDocumentCharset = defaultDocumentCharsetByPlatform;
+        }
+    }
+
+    /**
+     * The scheme.
+     */
+    protected char[] _scheme = null;
+
+    /**
+     * The opaque.
+     */
+    protected char[] _opaque = null;
+
+    /**
+     * The authority.
+     */
+    protected char[] _authority = null;
+
+    /**
+     * The userinfo.
+     */
+    protected char[] _userinfo = null;
+
+    /**
+     * The host.
+     */
+    protected char[] _host = null;
+
+    /**
+     * The port.
+     */
+    protected int _port = -1;
+
+    /**
+     * The path.
+     */
+    protected char[] _path = null;
+
+    /**
+     * The query.
+     */
+    protected char[] _query = null;
+
+    /**
+     * The fragment.
+     */
+    protected char[] _fragment = null;
+
+    /**
+     * The root path.
+     */
+    protected static final char[] rootPath = { '/' };
+
+    // ---------------------- Generous characters for each component validation
+
+    /**
+     * The percent "%" character always has the reserved purpose of being the
+     * escape indicator, it must be escaped as "%25" in order to be used as data
+     * within a URI.
+     */
+    protected static final BitSet percent = new BitSet(256);
+    // Static initializer for percent
+    static {
+        percent.set('%');
+    }
+
+    /**
+     * BitSet for digit.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * digit = &quot;0&quot; | &quot;1&quot; | &quot;2&quot; | &quot;3&quot; | &quot;4&quot; | &quot;5&quot; | &quot;6&quot; | &quot;7&quot; | &quot;8&quot; | &quot;9&quot;
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet digit = new BitSet(256);
+    // Static initializer for digit
+    static {
+        for (int i = '0'; i <= '9'; i++) {
+            digit.set(i);
+        }
+    }
+
+    /**
+     * BitSet for alpha.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * alpha = lowalpha | upalpha
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet alpha = new BitSet(256);
+    // Static initializer for alpha
+    static {
+        for (int i = 'a'; i <= 'z'; i++) {
+            alpha.set(i);
+        }
+        for (int i = 'A'; i <= 'Z'; i++) {
+            alpha.set(i);
+        }
+    }
+
+    /**
+     * BitSet for alphanum (join of alpha &amp; digit).
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * alphanum = alpha | digit
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet alphanum = new BitSet(256);
+    // Static initializer for alphanum
+    static {
+        alphanum.or(alpha);
+        alphanum.or(digit);
+    }
+
+    /**
+     * BitSet for hex.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * hex = digit | &quot;A&quot; | &quot;B&quot; | &quot;C&quot; | &quot;D&quot; | &quot;E&quot; | &quot;F&quot; | &quot;a&quot; | &quot;b&quot; | &quot;c&quot; | &quot;d&quot; | &quot;e&quot;
+     *     | &quot;f&quot;
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet hex = new BitSet(256);
+    // Static initializer for hex
+    static {
+        hex.or(digit);
+        for (int i = 'a'; i <= 'f'; i++) {
+            hex.set(i);
+        }
+        for (int i = 'A'; i <= 'F'; i++) {
+            hex.set(i);
+        }
+    }
+
+    /**
+     * BitSet for escaped.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * escaped       = "%" hex hex
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet escaped = new BitSet(256);
+    // Static initializer for escaped
+    static {
+        escaped.or(percent);
+        escaped.or(hex);
+    }
+
+    /**
+     * BitSet for mark.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * mark = &quot;-&quot; | &quot;_&quot; | &quot;.&quot; | &quot;!&quot; | &quot;&tilde;&quot; | &quot;*&quot; | &quot;'&quot; | &quot;(&quot; | &quot;)&quot;
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet mark = new BitSet(256);
+    // Static initializer for mark
+    static {
+        mark.set('-');
+        mark.set('_');
+        mark.set('.');
+        mark.set('!');
+        mark.set('~');
+        mark.set('*');
+        mark.set('\'');
+        mark.set('(');
+        mark.set(')');
+    }
+
+    /**
+     * Data characters that are allowed in a URI but do not have a reserved
+     * purpose are called unreserved.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * unreserved = alphanum | mark
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet unreserved = new BitSet(256);
+    // Static initializer for unreserved
+    static {
+        unreserved.or(alphanum);
+        unreserved.or(mark);
+    }
+
+    /**
+     * BitSet for reserved.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * reserved = &quot;;&quot; | &quot;/&quot; | &quot;?&quot; | &quot;:&quot; | &quot;@&quot; | &quot;&amp;&quot; | &quot;=&quot; | &quot;+&quot; | &quot;$&quot; | &quot;,&quot;
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet reserved = new BitSet(256);
+    // Static initializer for reserved
+    static {
+        reserved.set(';');
+        reserved.set('/');
+        reserved.set('?');
+        reserved.set(':');
+        reserved.set('@');
+        reserved.set('&');
+        reserved.set('=');
+        reserved.set('+');
+        reserved.set('$');
+        reserved.set(',');
+    }
+
+    /**
+     * BitSet for uric.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * uric = reserved | unreserved | escaped
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet uric = new BitSet(256);
+    // Static initializer for uric
+    static {
+        uric.or(reserved);
+        uric.or(unreserved);
+        uric.or(escaped);
+    }
+
+    /**
+     * BitSet for fragment (alias for uric).
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * fragment      = *uric
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet fragment = uric;
+
+    /**
+     * BitSet for query (alias for uric).
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * query         = *uric
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet query = uric;
+
+    /**
+     * BitSet for pchar.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * pchar = unreserved | escaped | &quot;:&quot; | &quot;@&quot; | &quot;&amp;&quot; | &quot;=&quot; | &quot;+&quot; | &quot;$&quot; | &quot;,&quot;
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet pchar = new BitSet(256);
+    // Static initializer for pchar
+    static {
+        pchar.or(unreserved);
+        pchar.or(escaped);
+        pchar.set(':');
+        pchar.set('@');
+        pchar.set('&');
+        pchar.set('=');
+        pchar.set('+');
+        pchar.set('$');
+        pchar.set(',');
+    }
+
+    /**
+     * BitSet for param (alias for pchar).
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * param         = *pchar
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet param = pchar;
+
+    /**
+     * BitSet for segment.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * segment       = *pchar *( ";" param )
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet segment = new BitSet(256);
+    // Static initializer for segment
+    static {
+        segment.or(pchar);
+        segment.set(';');
+        segment.or(param);
+    }
+
+    /**
+     * BitSet for path segments.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * path_segments = segment *( "/" segment )
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet path_segments = new BitSet(256);
+    // Static initializer for path_segments
+    static {
+        path_segments.set('/');
+        path_segments.or(segment);
+    }
+
+    /**
+     * URI absolute path.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * abs_path      = "/"  path_segments
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet abs_path = new BitSet(256);
+    // Static initializer for abs_path
+    static {
+        abs_path.set('/');
+        abs_path.or(path_segments);
+    }
+
+    /**
+     * URI bitset for encoding typical non-slash characters.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * uric_no_slash = unreserved | escaped | &quot;;&quot; | &quot;?&quot; | &quot;:&quot; | &quot;@&quot; | &quot;&amp;&quot; | &quot;=&quot; | &quot;+&quot;
+     *     | &quot;$&quot; | &quot;,&quot;
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet uric_no_slash = new BitSet(256);
+    // Static initializer for uric_no_slash
+    static {
+        uric_no_slash.or(unreserved);
+        uric_no_slash.or(escaped);
+        uric_no_slash.set(';');
+        uric_no_slash.set('?');
+        uric_no_slash.set(';');
+        uric_no_slash.set('@');
+        uric_no_slash.set('&');
+        uric_no_slash.set('=');
+        uric_no_slash.set('+');
+        uric_no_slash.set('$');
+        uric_no_slash.set(',');
+    }
+
+    /**
+     * URI bitset that combines uric_no_slash and uric.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * opaque_part = uric_no_slash * uric
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet opaque_part = new BitSet(256);
+    // Static initializer for opaque_part
+    static {
+        // it's generous. because first character must not include a slash
+        opaque_part.or(uric_no_slash);
+        opaque_part.or(uric);
+    }
+
+    /**
+     * URI bitset that combines absolute path and opaque part.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * path          = [ abs_path | opaque_part ]
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet path = new BitSet(256);
+    // Static initializer for path
+    static {
+        path.or(abs_path);
+        path.or(opaque_part);
+    }
+
+    /**
+     * Port, a logical alias for digit.
+     */
+    protected static final BitSet port = digit;
+
+    /**
+     * Bitset that combines digit and dot fo IPv$address.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * IPv4address   = 1*digit "." 1*digit "." 1*digit "." 1*digit
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet IPv4address = new BitSet(256);
+    // Static initializer for IPv4address
+    static {
+        IPv4address.or(digit);
+        IPv4address.set('.');
+    }
+
+    /**
+     * RFC 2373.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * IPv6address = hexpart [ ":" IPv4address ]
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet IPv6address = new BitSet(256);
+    // Static initializer for IPv6address reference
+    static {
+        IPv6address.or(hex); // hexpart
+        IPv6address.set(':');
+        IPv6address.or(IPv4address);
+    }
+
+    /**
+     * RFC 2732, 2373.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * IPv6reference   = "[" IPv6address "]"
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet IPv6reference = new BitSet(256);
+    // Static initializer for IPv6reference
+    static {
+        IPv6reference.set('[');
+        IPv6reference.or(IPv6address);
+        IPv6reference.set(']');
+    }
+
+    /**
+     * BitSet for toplabel.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * toplabel      = alpha | alpha *( alphanum | "-" ) alphanum
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet toplabel = new BitSet(256);
+    // Static initializer for toplabel
+    static {
+        toplabel.or(alphanum);
+        toplabel.set('-');
+    }
+
+    /**
+     * BitSet for domainlabel.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * domainlabel   = alphanum | alphanum *( alphanum | "-" ) alphanum
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet domainlabel = toplabel;
+
+    /**
+     * BitSet for hostname.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * hostname      = *( domainlabel "." ) toplabel [ "." ]
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet hostname = new BitSet(256);
+    // Static initializer for hostname
+    static {
+        hostname.or(toplabel);
+        // hostname.or(domainlabel);
+        hostname.set('.');
+    }
+
+    /**
+     * BitSet for host.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * host = hostname | IPv4address | IPv6reference
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet host = new BitSet(256);
+    // Static initializer for host
+    static {
+        host.or(hostname);
+        // host.or(IPv4address);
+        host.or(IPv6reference); // IPv4address
+    }
+
+    /**
+     * BitSet for hostport.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * hostport      = host [ ":" port ]
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet hostport = new BitSet(256);
+    // Static initializer for hostport
+    static {
+        hostport.or(host);
+        hostport.set(':');
+        hostport.or(port);
+    }
+
+    /**
+     * Bitset for userinfo.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * userinfo      = *( unreserved | escaped |
+     *                    ";" | ":" | "&amp;" | "=" | "+" | "$" | "," )
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet userinfo = new BitSet(256);
+    // Static initializer for userinfo
+    static {
+        userinfo.or(unreserved);
+        userinfo.or(escaped);
+        userinfo.set(';');
+        userinfo.set(':');
+        userinfo.set('&');
+        userinfo.set('=');
+        userinfo.set('+');
+        userinfo.set('$');
+        userinfo.set(',');
+    }
+
+    /**
+     * BitSet for within the userinfo component like user and password.
+     */
+    public static final BitSet within_userinfo = new BitSet(256);
+    // Static initializer for within_userinfo
+    static {
+        within_userinfo.or(userinfo);
+        within_userinfo.clear(';'); // reserved within authority
+        within_userinfo.clear(':');
+        within_userinfo.clear('@');
+        within_userinfo.clear('?');
+        within_userinfo.clear('/');
+    }
+
+    /**
+     * Bitset for server.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * server        = [ [ userinfo "@" ] hostport ]
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet server = new BitSet(256);
+    // Static initializer for server
+    static {
+        server.or(userinfo);
+        server.set('@');
+        server.or(hostport);
+    }
+
+    /**
+     * BitSet for reg_name.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * reg_name = 1 * (unreserved | escaped | &quot;$&quot; | &quot;,&quot; | &quot;;&quot; | &quot;:&quot; | &quot;@&quot; | &quot;&amp;&quot; | &quot;=&quot; | &quot;+&quot;)
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet reg_name = new BitSet(256);
+    // Static initializer for reg_name
+    static {
+        reg_name.or(unreserved);
+        reg_name.or(escaped);
+        reg_name.set('$');
+        reg_name.set(',');
+        reg_name.set(';');
+        reg_name.set(':');
+        reg_name.set('@');
+        reg_name.set('&');
+        reg_name.set('=');
+        reg_name.set('+');
+    }
+
+    /**
+     * BitSet for authority.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * authority = server | reg_name
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet authority = new BitSet(256);
+    // Static initializer for authority
+    static {
+        authority.or(server);
+        authority.or(reg_name);
+    }
+
+    /**
+     * BitSet for scheme.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * scheme = alpha * (alpha | digit | &quot;+&quot; | &quot;-&quot; | &quot;.&quot;)
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet scheme = new BitSet(256);
+    // Static initializer for scheme
+    static {
+        scheme.or(alpha);
+        scheme.or(digit);
+        scheme.set('+');
+        scheme.set('-');
+        scheme.set('.');
+    }
+
+    /**
+     * BitSet for rel_segment.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * rel_segment = 1 * (unreserved | escaped | &quot;;&quot; | &quot;@&quot; | &quot;&amp;&quot; | &quot;=&quot; | &quot;+&quot; | &quot;$&quot; | &quot;,&quot;)
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet rel_segment = new BitSet(256);
+    // Static initializer for rel_segment
+    static {
+        rel_segment.or(unreserved);
+        rel_segment.or(escaped);
+        rel_segment.set(';');
+        rel_segment.set('@');
+        rel_segment.set('&');
+        rel_segment.set('=');
+        rel_segment.set('+');
+        rel_segment.set('$');
+        rel_segment.set(',');
+    }
+
+    /**
+     * BitSet for rel_path.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * rel_path = rel_segment[abs_path]
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet rel_path = new BitSet(256);
+    // Static initializer for rel_path
+    static {
+        rel_path.or(rel_segment);
+        rel_path.or(abs_path);
+    }
+
+    /**
+     * BitSet for net_path.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * net_path      = "//" authority [ abs_path ]
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet net_path = new BitSet(256);
+    // Static initializer for net_path
+    static {
+        net_path.set('/');
+        net_path.or(authority);
+        net_path.or(abs_path);
+    }
+
+    /**
+     * BitSet for hier_part.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * hier_part     = ( net_path | abs_path ) [ "?" query ]
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet hier_part = new BitSet(256);
+    // Static initializer for hier_part
+    static {
+        hier_part.or(net_path);
+        hier_part.or(abs_path);
+        // hier_part.set('?'); aleady included
+        hier_part.or(query);
+    }
+
+    /**
+     * BitSet for relativeURI.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * relativeURI   = ( net_path | abs_path | rel_path ) [ "?" query ]
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet relativeURI = new BitSet(256);
+    // Static initializer for relativeURI
+    static {
+        relativeURI.or(net_path);
+        relativeURI.or(abs_path);
+        relativeURI.or(rel_path);
+        // relativeURI.set('?'); aleady included
+        relativeURI.or(query);
+    }
+
+    /**
+     * BitSet for absoluteURI.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * absoluteURI   = scheme ":" ( hier_part | opaque_part )
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet absoluteURI = new BitSet(256);
+    // Static initializer for absoluteURI
+    static {
+        absoluteURI.or(scheme);
+        absoluteURI.set(':');
+        absoluteURI.or(hier_part);
+        absoluteURI.or(opaque_part);
+    }
+
+    /**
+     * BitSet for URI-reference.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ]
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     */
+    protected static final BitSet URI_reference = new BitSet(256);
+    // Static initializer for URI_reference
+    static {
+        URI_reference.or(absoluteURI);
+        URI_reference.or(relativeURI);
+        URI_reference.set('#');
+        URI_reference.or(fragment);
+    }
+
+    // ---------------------------- Characters disallowed within the URI syntax
+    // Excluded US-ASCII Characters are like control, space, delims and unwise
+
+    /**
+     * BitSet for control.
+     */
+    public static final BitSet control = new BitSet(256);
+    // Static initializer for control
+    static {
+        for (int i = 0; i <= 0x1F; i++) {
+            control.set(i);
+        }
+        control.set(0x7F);
+    }
+
+    /**
+     * BitSet for space.
+     */
+    public static final BitSet space = new BitSet(256);
+    // Static initializer for space
+    static {
+        space.set(0x20);
+    }
+
+    /**
+     * BitSet for delims.
+     */
+    public static final BitSet delims = new BitSet(256);
+    // Static initializer for delims
+    static {
+        delims.set('<');
+        delims.set('>');
+        delims.set('#');
+        delims.set('%');
+        delims.set('"');
+    }
+
+    /**
+     * BitSet for unwise.
+     */
+    public static final BitSet unwise = new BitSet(256);
+    // Static initializer for unwise
+    static {
+        unwise.set('{');
+        unwise.set('}');
+        unwise.set('|');
+        unwise.set('\\');
+        unwise.set('^');
+        unwise.set('[');
+        unwise.set(']');
+        unwise.set('`');
+    }
+
+    /**
+     * Disallowed rel_path before escaping.
+     */
+    public static final BitSet disallowed_rel_path = new BitSet(256);
+    // Static initializer for disallowed_rel_path
+    static {
+        disallowed_rel_path.or(uric);
+        disallowed_rel_path.andNot(rel_path);
+    }
+
+    /**
+     * Disallowed opaque_part before escaping.
+     */
+    public static final BitSet disallowed_opaque_part = new BitSet(256);
+    // Static initializer for disallowed_opaque_part
+    static {
+        disallowed_opaque_part.or(uric);
+        disallowed_opaque_part.andNot(opaque_part);
+    }
+
+    // ----------------------- Characters allowed within and for each component
+
+    /**
+     * Those characters that are allowed for the authority component.
+     */
+    public static final BitSet allowed_authority = new BitSet(256);
+    // Static initializer for allowed_authority
+    static {
+        allowed_authority.or(authority);
+        allowed_authority.clear('%');
+    }
+
+    /**
+     * Those characters that are allowed for the opaque_part.
+     */
+    public static final BitSet allowed_opaque_part = new BitSet(256);
+    // Static initializer for allowed_opaque_part
+    static {
+        allowed_opaque_part.or(opaque_part);
+        allowed_opaque_part.clear('%');
+    }
+
+    /**
+     * Those characters that are allowed for the reg_name.
+     */
+    public static final BitSet allowed_reg_name = new BitSet(256);
+    // Static initializer for allowed_reg_name
+    static {
+        allowed_reg_name.or(reg_name);
+        // allowed_reg_name.andNot(percent);
+        allowed_reg_name.clear('%');
+    }
+
+    /**
+     * Those characters that are allowed for the userinfo component.
+     */
+    public static final BitSet allowed_userinfo = new BitSet(256);
+    // Static initializer for allowed_userinfo
+    static {
+        allowed_userinfo.or(userinfo);
+        // allowed_userinfo.andNot(percent);
+        allowed_userinfo.clear('%');
+    }
+
+    /**
+     * Those characters that are allowed for within the userinfo component.
+     */
+    public static final BitSet allowed_within_userinfo = new BitSet(256);
+    // Static initializer for allowed_within_userinfo
+    static {
+        allowed_within_userinfo.or(within_userinfo);
+        allowed_within_userinfo.clear('%');
+    }
+
+    /**
+     * Those characters that are allowed for the IPv6reference component. The
+     * characters '[', ']' in IPv6reference should be excluded.
+     */
+    public static final BitSet allowed_IPv6reference = new BitSet(256);
+    // Static initializer for allowed_IPv6reference
+    static {
+        allowed_IPv6reference.or(IPv6reference);
+        // allowed_IPv6reference.andNot(unwise);
+        allowed_IPv6reference.clear('[');
+        allowed_IPv6reference.clear(']');
+    }
+
+    /**
+     * Those characters that are allowed for the host component. The characters
+     * '[', ']' in IPv6reference should be excluded.
+     */
+    public static final BitSet allowed_host = new BitSet(256);
+    // Static initializer for allowed_host
+    static {
+        allowed_host.or(hostname);
+        allowed_host.or(allowed_IPv6reference);
+    }
+
+    /**
+     * Those characters that are allowed for the authority component.
+     */
+    public static final BitSet allowed_within_authority = new BitSet(256);
+    // Static initializer for allowed_within_authority
+    static {
+        allowed_within_authority.or(server);
+        allowed_within_authority.or(reg_name);
+        allowed_within_authority.clear(';');
+        allowed_within_authority.clear(':');
+        allowed_within_authority.clear('@');
+        allowed_within_authority.clear('?');
+        allowed_within_authority.clear('/');
+    }
+
+    /**
+     * Those characters that are allowed for the abs_path.
+     */
+    public static final BitSet allowed_abs_path = new BitSet(256);
+    // Static initializer for allowed_abs_path
+    static {
+        allowed_abs_path.or(abs_path);
+        // allowed_abs_path.set('/'); // aleady included
+        allowed_abs_path.andNot(percent);
+        allowed_abs_path.clear('+');
+    }
+
+    /**
+     * Those characters that are allowed for the rel_path.
+     */
+    public static final BitSet allowed_rel_path = new BitSet(256);
+    // Static initializer for allowed_rel_path
+    static {
+        allowed_rel_path.or(rel_path);
+        allowed_rel_path.clear('%');
+        allowed_rel_path.clear('+');
+    }
+
+    /**
+     * Those characters that are allowed within the path.
+     */
+    public static final BitSet allowed_within_path = new BitSet(256);
+    // Static initializer for allowed_within_path
+    static {
+        allowed_within_path.or(abs_path);
+        allowed_within_path.clear('/');
+        allowed_within_path.clear(';');
+        allowed_within_path.clear('=');
+        allowed_within_path.clear('?');
+    }
+
+    /**
+     * Those characters that are allowed for the query component.
+     */
+    public static final BitSet allowed_query = new BitSet(256);
+    // Static initializer for allowed_query
+    static {
+        allowed_query.or(uric);
+        allowed_query.clear('%');
+    }
+
+    /**
+     * Those characters that are allowed within the query component.
+     */
+    public static final BitSet allowed_within_query = new BitSet(256);
+    // Static initializer for allowed_within_query
+    static {
+        allowed_within_query.or(allowed_query);
+        allowed_within_query.andNot(reserved); // excluded 'reserved'
+    }
+
+    /**
+     * Those characters that are allowed for the fragment component.
+     */
+    public static final BitSet allowed_fragment = new BitSet(256);
+    // Static initializer for allowed_fragment
+    static {
+        allowed_fragment.or(uric);
+        allowed_fragment.clear('%');
+    }
+
+    // ------------------------------------------- Flags for this URI-reference
+
+    // TODO: Figure out what all these variables are for and provide javadoc
+
+    // URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ]
+    // absoluteURI = scheme ":" ( hier_part | opaque_part )
+    protected boolean _is_hier_part;
+
+    protected boolean _is_opaque_part;
+
+    // relativeURI = ( net_path | abs_path | rel_path ) [ "?" query ]
+    // hier_part = ( net_path | abs_path ) [ "?" query ]
+    protected boolean _is_net_path;
+
+    protected boolean _is_abs_path;
+
+    protected boolean _is_rel_path;
+
+    // net_path = "//" authority [ abs_path ]
+    // authority = server | reg_name
+    protected boolean _is_reg_name;
+
+    protected boolean _is_server; // = _has_server
+
+    // server = [ [ userinfo "@" ] hostport ]
+    // host = hostname | IPv4address | IPv6reference
+    protected boolean _is_hostname;
+
+    protected boolean _is_IPv4address;
+
+    protected boolean _is_IPv6reference;
+
+    // ------------------------------------------ Character and escape encoding
+
+    /**
+     * Encodes URI string. This is a two mapping, one from original characters
+     * to octets, and subsequently a second from octets to URI characters:
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     *   original character sequence->octet sequence->URI character sequence
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     * An escaped octet is encoded as a character triplet, consisting of the
+     * percent character "%" followed by the two hexadecimal digits representing
+     * the octet code. For example, "%20" is the escaped encoding for the
+     * US-ASCII space character.
+     * <p>
+     * Conversion from the local filesystem character set to UTF-8 will normally
+     * involve a two step process. First convert the local character set to the
+     * UCS; then convert the UCS to UTF-8. The first step in the process can be
+     * performed by maintaining a mapping table that includes the local
+     * character set code and the corresponding UCS code. The next step is to
+     * convert the UCS character code to the UTF-8 encoding.
+     * <p>
+     * Mapping between vendor codepages can be done in a very similar manner as
+     * described above.
+     * <p>
+     * The only time escape encodings can allowedly be made is when a URI is
+     * being created from its component parts. The escape and validate methods
+     * are internally performed within this method.
+     *
+     * @param original the original character sequence
+     * @param allowed those characters that are allowed within a component
+     * @param charset the protocol charset
+     * @return URI character sequence
+     * @throws URIException null component or unsupported character encoding
+     */
+
+    protected static char[] encode(String original, BitSet allowed,
+            String charset) throws URIException {
+        if (original == null) {
+            throw new IllegalArgumentException(
+                "Original string may not be null");
+        }
+        if (allowed == null) {
+            throw new IllegalArgumentException("Allowed bitset may not be null");
+        }
+        byte[] rawdata = encodeUrl(allowed, getBytes(original, charset));
+        return getAsciiString(rawdata).toCharArray();
+    }
+
+    /**
+     * Decodes URI encoded string. This is a two mapping, one from URI
+     * characters to octets, and subsequently a second from octets to original
+     * characters:
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     *   URI character sequence->octet sequence->original character sequence
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     * A URI must be separated into its components before the escaped characters
+     * within those components can be allowedly decoded.
+     * <p>
+     * Notice that there is a chance that URI characters that are non UTF-8 may
+     * be parsed as valid UTF-8. A recent non-scientific analysis found that EUC
+     * encoded Japanese words had a 2.7% false reading; SJIS had a 0.0005% false
+     * reading; other encoding such as ASCII or KOI-8 have a 0% false reading.
+     * <p>
+     * The percent "%" character always has the reserved purpose of being the
+     * escape indicator, it must be escaped as "%25" in order to be used as data
+     * within a URI.
+     * <p>
+     * The unescape method is internally performed within this method.
+     *
+     * @param component the URI character sequence
+     * @param charset the protocol charset
+     * @return original character sequence
+     * @throws URIException incomplete trailing escape pattern or unsupported
+     *             character encoding
+     */
+    protected static String decode(char[] component, String charset)
+            throws URIException {
+        if (component == null) {
+            throw new IllegalArgumentException(
+                "Component array of chars may not be null");
+        }
+        return decode(new String(component), charset);
+    }
+
+    /**
+     * Decodes URI encoded string. This is a two mapping, one from URI
+     * characters to octets, and subsequently a second from octets to original
+     * characters:
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     *   URI character sequence->octet sequence->original character sequence
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     * A URI must be separated into its components before the escaped characters
+     * within those components can be allowedly decoded.
+     * <p>
+     * Notice that there is a chance that URI characters that are non UTF-8 may
+     * be parsed as valid UTF-8. A recent non-scientific analysis found that EUC
+     * encoded Japanese words had a 2.7% false reading; SJIS had a 0.0005% false
+     * reading; other encoding such as ASCII or KOI-8 have a 0% false reading.
+     * <p>
+     * The percent "%" character always has the reserved purpose of being the
+     * escape indicator, it must be escaped as "%25" in order to be used as data
+     * within a URI.
+     * <p>
+     * The unescape method is internally performed within this method.
+     *
+     * @param component the URI character sequence
+     * @param charset the protocol charset
+     * @return original character sequence
+     * @throws URIException incomplete trailing escape pattern or unsupported
+     *             character encoding
+     * @since 3.0
+     */
+    protected static String decode(String component, String charset)
+            throws URIException {
+        if (component == null) {
+            throw new IllegalArgumentException(
+                "Component array of chars may not be null");
+        }
+        byte[] rawdata = decodeUrl(getAsciiBytes(component));
+        return getString(rawdata, charset);
+    }
+
+    /**
+     * Pre-validate the unescaped URI string within a specific component.
+     *
+     * @param component the component string within the component
+     * @param disallowed those characters disallowed within the component
+     * @return if true, it doesn't have the disallowed characters if false, the
+     *         component is undefined or an incorrect one
+     */
+    protected boolean prevalidate(String component, BitSet disallowed) {
+        // prevalidate the given component by disallowed characters
+        if (component == null) {
+            return false; // undefined
+        }
+        char[] target = component.toCharArray();
+        for (int i = 0; i < target.length; i++) {
+            if (disallowed.get(target[i])) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Validate the URI characters within a specific component. The component
+     * must be performed after escape encoding. Or it doesn't include escaped
+     * characters.
+     *
+     * @param component the characters sequence within the component
+     * @param generous those characters that are allowed within a component
+     * @return if true, it's the correct URI character sequence
+     */
+    protected boolean validate(char[] component, BitSet generous) {
+        // validate each component by generous characters
+        return validate(component, 0, -1, generous);
+    }
+
+    /**
+     * Validate the URI characters within a specific component. The component
+     * must be performed after escape encoding. Or it doesn't include escaped
+     * characters.
+     * <p>
+     * It's not that much strict, generous. The strict validation might be
+     * performed before being called this method.
+     *
+     * @param component the characters sequence within the component
+     * @param soffset the starting offset of the given component
+     * @param eoffset the ending offset of the given component if -1, it means
+     *            the length of the component
+     * @param generous those characters that are allowed within a component
+     * @return if true, it's the correct URI character sequence
+     */
+    protected boolean validate(char[] component, int soffset, int eoffset,
+            BitSet generous) {
+        // validate each component by generous characters
+        if (eoffset == -1) {
+            eoffset = component.length - 1;
+        }
+        for (int i = soffset; i <= eoffset; i++) {
+            if (!generous.get(component[i])) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * In order to avoid any possilbity of conflict with non-ASCII characters,
+     * Parse a URI reference as a <code>String</code> with the character
+     * encoding of the local system or the document.
+     * <p>
+     * The following line is the regular expression for breaking-down a URI
+     * reference into its components.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     *   ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
+     *    12            3  4          5       6  7        8 9
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     * For example, matching the above expression to
+     * http://jakarta.apache.org/ietf/uri/#Related results in the following
+     * subexpression matches:
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     *               $1 = http:
+     *  scheme    =  $2 = http
+     *               $3 = //jakarta.apache.org
+     *  authority =  $4 = jakarta.apache.org
+     *  path      =  $5 = /ietf/uri/
+     *               $6 = <undefined>
+     *  query     =  $7 = <undefined>
+     *               $8 = #Related
+     *  fragment  =  $9 = Related
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     *
+     * @param original the original character sequence
+     * @param escaped <code>true</code> if <code>original</code> is escaped
+     * @throws URIException If an error occurs.
+     */
+    protected void parseUriReference(String original, boolean escaped)
+            throws URIException {
+
+        // validate and contruct the URI character sequence
+        if (original == null) {
+            throw new URIException("URI-Reference required");
+        }
+
+        /*
+         * @ ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
+         */
+        String tmp = original.trim();
+
+        /*
+         * The length of the string sequence of characters. It may not be equal
+         * to the length of the byte array.
+         */
+        int length = tmp.length();
+
+        /*
+         * Remove the delimiters like angle brackets around an URI.
+         */
+        if (length > 0) {
+            char[] firstDelimiter = { tmp.charAt(0) };
+            if (validate(firstDelimiter, delims)) {
+                if (length >= 2) {
+                    char[] lastDelimiter = { tmp.charAt(length - 1) };
+                    if (validate(lastDelimiter, delims)) {
+                        tmp = tmp.substring(1, length - 1);
+                        length = length - 2;
+                    }
+                }
+            }
+        }
+
+        /*
+         * The starting index
+         */
+        int from = 0;
+
+        /*
+         * The test flag whether the URI is started from the path component.
+         */
+        boolean isStartedFromPath = false;
+        int atColon = tmp.indexOf(':');
+        int atSlash = tmp.indexOf('/');
+        if ((atColon <= 0 && !tmp.startsWith("//"))
+            || (atSlash >= 0 && atSlash < atColon)) {
+            isStartedFromPath = true;
+        }
+
+        /*
+         * <p><blockquote><pre>
+         * @@@@@@@@ ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
+         * </pre></blockquote><p>
+         */
+        int at = indexFirstOf(tmp, isStartedFromPath ? "/?#" : ":/?#", from);
+        if (at == -1) {
+            at = 0;
+        }
+
+        /*
+         * Parse the scheme. <p><blockquote><pre> scheme = $2 = http
+         * @ ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
+         * </pre></blockquote><p>
+         */
+        if (at > 0 && at < length && tmp.charAt(at) == ':') {
+            char[] target = tmp.substring(0, at).toLowerCase().toCharArray();
+            if (validate(target, scheme)) {
+                _scheme = target;
+            } else {
+                throw new URIException("incorrect scheme");
+            }
+            from = ++at;
+        }
+
+        /*
+         * Parse the authority component. <p><blockquote><pre> authority = $4 =
+         * jakarta.apache.org
+         * @@ ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
+         * </pre></blockquote><p>
+         */
+        // Reset flags
+        _is_net_path = _is_abs_path = _is_rel_path = _is_hier_part = false;
+        if (0 <= at && at < length && tmp.charAt(at) == '/') {
+            // Set flag
+            _is_hier_part = true;
+            if (at + 2 < length && tmp.charAt(at + 1) == '/'
+                && !isStartedFromPath) {
+                // the temporary index to start the search from
+                int next = indexFirstOf(tmp, "/?#", at + 2);
+                if (next == -1) {
+                    next = (tmp.substring(at + 2).length() == 0)
+                            ? at + 2
+                            : tmp.length();
+                }
+                parseAuthority(tmp.substring(at + 2, next), escaped);
+                from = at = next;
+                // Set flag
+                _is_net_path = true;
+            }
+            if (from == at) {
+                // Set flag
+                _is_abs_path = true;
+            }
+        }
+
+        /*
+         * Parse the path component. <p><blockquote><pre> path = $5 = /ietf/uri/
+         * @@@@@@ ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
+         * </pre></blockquote><p>
+         */
+        if (from < length) {
+            // rel_path = rel_segment [ abs_path ]
+            int next = indexFirstOf(tmp, "?#", from);
+            if (next == -1) {
+                next = tmp.length();
+            }
+            if (!_is_abs_path) {
+                if (!escaped
+                    && prevalidate(tmp.substring(from, next),
+                        disallowed_rel_path)
+                    || escaped
+                    && validate(tmp.substring(from, next).toCharArray(),
+                        rel_path)) {
+                    // Set flag
+                    _is_rel_path = true;
+                } else if (!escaped
+                    && prevalidate(tmp.substring(from, next),
+                        disallowed_opaque_part)
+                    || escaped
+                    && validate(tmp.substring(from, next).toCharArray(),
+                        opaque_part)) {
+                    // Set flag
+                    _is_opaque_part = true;
+                } else {
+                    // the path component may be empty
+                    _path = null;
+                }
+            }
+            String s = tmp.substring(from, next);
+            if (escaped) {
+                setRawPath(s.toCharArray());
+            } else {
+                setPath(s);
+            }
+            at = next;
+        }
+
+        // set the charset to do escape encoding
+        String charset = getProtocolCharset();
+
+        /*
+         * Parse the query component. <p><blockquote><pre> query = $7 =
+         * <undefined>
+         * @@@@@@@@@ ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
+         * </pre></blockquote><p>
+         */
+        if (0 <= at && at + 1 < length && tmp.charAt(at) == '?') {
+            int next = tmp.indexOf('#', at + 1);
+            if (next == -1) {
+                next = tmp.length();
+            }
+            if (escaped) {
+                _query = tmp.substring(at + 1, next).toCharArray();
+                if (!validate(_query, uric)) {
+                    throw new URIException("Invalid query");
+                }
+            } else {
+                _query = encode(tmp.substring(at + 1, next), allowed_query,
+                    charset);
+            }
+            at = next;
+        }
+
+        /*
+         * Parse the fragment component. <p><blockquote><pre> fragment = $9 =
+         * Related
+         * @@@@@@@@ ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
+         * </pre></blockquote><p>
+         */
+        if (0 <= at && at + 1 <= length && tmp.charAt(at) == '#') {
+            if (at + 1 == length) { // empty fragment
+                _fragment = "".toCharArray();
+            } else {
+                _fragment = (escaped)
+                        ? tmp.substring(at + 1).toCharArray()
+                        : encode(tmp.substring(at + 1), allowed_fragment,
+                            charset);
+            }
+        }
+
+        // set this URI.
+        setURI();
+    }
+
+    /**
+     * Get the earlier index that to be searched for the first occurrance in one
+     * of any of the given string.
+     *
+     * @param s the string to be indexed
+     * @param delims the delimiters used to index
+     * @return the earlier index if there are delimiters
+     */
+    protected int indexFirstOf(String s, String delims) {
+        return indexFirstOf(s, delims, -1);
+    }
+
+    /**
+     * Get the earlier index that to be searched for the first occurrance in one
+     * of any of the given string.
+     *
+     * @param s the string to be indexed
+     * @param delims the delimiters used to index
+     * @param offset the from index
+     * @return the earlier index if there are delimiters
+     */
+    protected int indexFirstOf(String s, String delims, int offset) {
+        if (s == null || s.length() == 0) {
+            return -1;
+        }
+        if (delims == null || delims.length() == 0) {
+            return -1;
+        }
+        // check boundaries
+        if (offset < 0) {
+            offset = 0;
+        } else if (offset > s.length()) {
+            return -1;
+        }
+        // s is never null
+        int min = s.length();
+        char[] delim = delims.toCharArray();
+        for (int i = 0; i < delim.length; i++) {
+            int at = s.indexOf(delim[i], offset);
+            if (at >= 0 && at < min) {
+                min = at;
+            }
+        }
+        return (min == s.length()) ? -1 : min;
+    }
+
+    /**
+     * Get the earlier index that to be searched for the first occurrance in one
+     * of any of the given array.
+     *
+     * @param s the character array to be indexed
+     * @param delim the delimiter used to index
+     * @return the ealier index if there are a delimiter
+     */
+    protected int indexFirstOf(char[] s, char delim) {
+        return indexFirstOf(s, delim, 0);
+    }
+
+    /**
+     * Get the earlier index that to be searched for the first occurrance in one
+     * of any of the given array.
+     *
+     * @param s the character array to be indexed
+     * @param delim the delimiter used to index
+     * @param offset The offset.
+     * @return the ealier index if there is a delimiter
+     */
+    protected int indexFirstOf(char[] s, char delim, int offset) {
+        if (s == null || s.length == 0) {
+            return -1;
+        }
+        // check boundaries
+        if (offset < 0) {
+            offset = 0;
+        } else if (offset > s.length) {
+            return -1;
+        }
+        for (int i = offset; i < s.length; i++) {
+            if (s[i] == delim) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * Parse the authority component.
+     *
+     * @param original the original character sequence of authority component
+     * @param escaped <code>true</code> if <code>original</code> is escaped
+     * @throws URIException If an error occurs.
+     */
+    protected void parseAuthority(String original, boolean escaped)
+            throws URIException {
+
+        // Reset flags
+        _is_reg_name = _is_server = _is_hostname = _is_IPv4address = _is_IPv6reference = false;
+
+        // set the charset to do escape encoding
+        String charset = getProtocolCharset();
+
+        boolean hasPort = true;
+        int from = 0;
+        int next = original.indexOf('@');
+        if (next != -1) { // neither -1 and 0
+            // each protocol extented from URI supports the specific userinfo
+            _userinfo = (escaped)
+                    ? original.substring(0, next).toCharArray()
+                    : encode(original.substring(0, next), allowed_userinfo,
+                        charset);
+            from = next + 1;
+        }
+        next = original.indexOf('[', from);
+        if (next >= from) {
+            next = original.indexOf(']', from);
+            if (next == -1) {
+                throw new URIException(URIException.PARSING, "IPv6reference");
+            }
+            next++;
+            // In IPv6reference, '[', ']' should be excluded
+            _host = (escaped)
+                    ? original.substring(from, next).toCharArray()
+                    : encode(original.substring(from, next),
+                        allowed_IPv6reference, charset);
+            // Set flag
+            _is_IPv6reference = true;
+        } else { // only for !_is_IPv6reference
+            next = original.indexOf(':', from);
+            if (next == -1) {
+                next = original.length();
+                hasPort = false;
+            }
+            // REMINDME: it doesn't need the pre-validation
+            _host = original.substring(from, next).toCharArray();
+            if (validate(_host, IPv4address)) {
+                // Set flag
+                _is_IPv4address = true;
+            } else if (validate(_host, hostname)) {
+                // Set flag
+                _is_hostname = true;
+            } else {
+                // Set flag
+                _is_reg_name = true;
+            }
+        }
+        if (_is_reg_name) {
+            // Reset flags for a server-based naming authority
+            _is_server = _is_hostname = _is_IPv4address = _is_IPv6reference = false;
+            // set a registry-based naming authority
+            if (escaped) {
+                _authority = original.toCharArray();
+                if (!validate(_authority, reg_name)) {
+                    throw new URIException("Invalid authority");
+                }
+            } else {
+                _authority = encode(original, allowed_reg_name, charset);
+            }
+        } else {
+            if (original.length() - 1 > next && hasPort
+                && original.charAt(next) == ':') { // not empty
+                from = next + 1;
+                try {
+                    _port = Integer.parseInt(original.substring(from));
+                } catch (NumberFormatException error) {
+                    throw new URIException(URIException.PARSING,
+                        "invalid port number");
+                }
+            }
+            // set a server-based naming authority
+            StringBuilder buf = new StringBuilder();
+            if (_userinfo != null) { // has_userinfo
+                buf.append(_userinfo);
+                buf.append('@');
+            }
+            if (_host != null) {
+                buf.append(_host);
+                if (_port != -1) {
+                    buf.append(':');
+                    buf.append(_port);
+                }
+            }
+            _authority = buf.toString().toCharArray();
+            // Set flag
+            _is_server = true;
+        }
+    }
+
+    /**
+     * Once it's parsed successfully, set this URI.
+     *
+     * @see #getRawURI
+     */
+    protected void setURI() {
+        // set _uri
+        StringBuilder buf = new StringBuilder();
+        // ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
+        if (_scheme != null) {
+            buf.append(_scheme);
+            buf.append(':');
+        }
+        if (_is_net_path) {
+            buf.append("//");
+            if (_authority != null) { // has_authority
+                buf.append(_authority);
+            }
+        }
+        if (_opaque != null && _is_opaque_part) {
+            buf.append(_opaque);
+        } else if (_path != null) {
+            // _is_hier_part or _is_relativeURI
+            if (_path.length != 0) {
+                buf.append(_path);
+            }
+        }
+        if (_query != null) { // has_query
+            buf.append('?');
+            buf.append(_query);
+        }
+        // ignore the fragment identifier
+        _uri = buf.toString().toCharArray();
+        hash = 0;
+    }
+
+    // ----------------------------------------------------------- Test methods
+
+    /**
+     * Tell whether or not this URI is absolute.
+     *
+     * @return true iif this URI is absoluteURI
+     */
+    public boolean isAbsoluteURI() {
+        return (_scheme != null);
+    }
+
+    /**
+     * Tell whether or not this URI is relative.
+     *
+     * @return true iif this URI is relativeURI
+     */
+    public boolean isRelativeURI() {
+        return (_scheme == null);
+    }
+
+    /**
+     * Tell whether or not the absoluteURI of this URI is hier_part.
+     *
+     * @return true iif the absoluteURI is hier_part
+     */
+    public boolean isHierPart() {
+        return _is_hier_part;
+    }
+
+    /**
+     * Tell whether or not the absoluteURI of this URI is opaque_part.
+     *
+     * @return true iif the absoluteURI is opaque_part
+     */
+    public boolean isOpaquePart() {
+        return _is_opaque_part;
+    }
+
+    /**
+     * Tell whether or not the relativeURI or heir_part of this URI is net_path.
+     * It's the same function as the has_authority() method.
+     *
+     * @return true iif the relativeURI or heir_part is net_path
+     * @see #hasAuthority
+     */
+    public boolean isNetPath() {
+        return _is_net_path || (_authority != null);
+    }
+
+    /**
+     * Tell whether or not the relativeURI or hier_part of this URI is abs_path.
+     *
+     * @return true iif the relativeURI or hier_part is abs_path
+     */
+    public boolean isAbsPath() {
+        return _is_abs_path;
+    }
+
+    /**
+     * Tell whether or not the relativeURI of this URI is rel_path.
+     *
+     * @return true iif the relativeURI is rel_path
+     */
+    public boolean isRelPath() {
+        return _is_rel_path;
+    }
+
+    /**
+     * Tell whether or not this URI has authority. It's the same function as the
+     * is_net_path() method.
+     *
+     * @return true iif this URI has authority
+     * @see #isNetPath
+     */
+    public boolean hasAuthority() {
+        return (_authority != null) || _is_net_path;
+    }
+
+    /**
+     * Tell whether or not the authority component of this URI is reg_name.
+     *
+     * @return true iif the authority component is reg_name
+     */
+    public boolean isRegName() {
+        return _is_reg_name;
+    }
+
+    /**
+     * Tell whether or not the authority component of this URI is server.
+     *
+     * @return true iif the authority component is server
+     */
+    public boolean isServer() {
+        return _is_server;
+    }
+
+    /**
+     * Tell whether or not this URI has userinfo.
+     *
+     * @return true iif this URI has userinfo
+     */
+    public boolean hasUserinfo() {
+        return (_userinfo != null);
+    }
+
+    /**
+     * Tell whether or not the host part of this URI is hostname.
+     *
+     * @return true iif the host part is hostname
+     */
+    public boolean isHostname() {
+        return _is_hostname;
+    }
+
+    /**
+     * Tell whether or not the host part of this URI is IPv4address.
+     *
+     * @return true iif the host part is IPv4address
+     */
+    public boolean isIPv4address() {
+        return _is_IPv4address;
+    }
+
+    /**
+     * Tell whether or not the host part of this URI is IPv6reference.
+     *
+     * @return true iif the host part is IPv6reference
+     */
+    public boolean isIPv6reference() {
+        return _is_IPv6reference;
+    }
+
+    /**
+     * Tell whether or not this URI has query.
+     *
+     * @return true iif this URI has query
+     */
+    public boolean hasQuery() {
+        return (_query != null);
+    }
+
+    /**
+     * Tell whether or not this URI has fragment.
+     *
+     * @return true iif this URI has fragment
+     */
+    public boolean hasFragment() {
+        return (_fragment != null);
+    }
+
+    // ---------------------------------------------------------------- Charset
+
+    /**
+     * Set the default charset of the protocol.
+     * <p>
+     * The character set used to store files SHALL remain a local decision and
+     * MAY depend on the capability of local operating systems. Prior to the
+     * exchange of URIs they SHOULD be converted into a ISO/IEC 10646 format and
+     * UTF-8 encoded. This approach, while allowing international exchange of
+     * URIs, will still allow backward compatibility with older systems because
+     * the code set positions for ASCII characters are identical to the one byte
+     * sequence in UTF-8.
+     * <p>
+     * An individual URI scheme may require a single charset, define a default
+     * charset, or provide a way to indicate the charset used.
+     * <p>
+     * Always all the time, the setter method is always succeeded and throws
+     * <code>DefaultCharsetChanged</code> exception. So API programmer must
+     * follow the following way: <code><pre>
+     *  import org.apache.util.URI$DefaultCharsetChanged;
+     *      .
+     *      .
+     *      .
+     *  try {
+     *      URI.setDefaultProtocolCharset("UTF-8");
+     *  } catch (DefaultCharsetChanged cc) {
+     *      // CASE 1: the exception could be ignored, when it is set by user
+     *      if (cc.getReasonCode() == DefaultCharsetChanged.PROTOCOL_CHARSET) {
+     *      // CASE 2: let user know the default protocol charset changed
+     *      } else {
+     *      // CASE 2: let user know the default document charset changed
+     *      }
+     *  }
+     *  </pre></code> The API programmer is responsible to set the correct
+     * charset. And each application should remember its own charset to support.
+     *
+     * @param charset the default charset for each protocol
+     * @throws DefaultCharsetChanged default charset changed
+     */
+    public static void setDefaultProtocolCharset(String charset)
+            throws DefaultCharsetChanged {
+
+        defaultProtocolCharset = charset;
+        throw new DefaultCharsetChanged(DefaultCharsetChanged.PROTOCOL_CHARSET,
+            "the default protocol charset changed");
+    }
+
+    /**
+     * Get the default charset of the protocol.
+     * <p>
+     * An individual URI scheme may require a single charset, define a default
+     * charset, or provide a way to indicate the charset used.
+     * <p>
+     * To work globally either requires support of a number of character sets
+     * and to be able to convert between them, or the use of a single preferred
+     * character set. For support of global compatibility it is STRONGLY
+     * RECOMMENDED that clients and servers use UTF-8 encoding when exchanging
+     * URIs.
+     *
+     * @return the default charset string
+     */
+    public static String getDefaultProtocolCharset() {
+        return defaultProtocolCharset;
+    }
+
+    /**
+     * Get the protocol charset used by this current URI instance. It was set by
+     * the constructor for this instance. If it was not set by contructor, it
+     * will return the default protocol charset.
+     *
+     * @return the protocol charset string
+     * @see #getDefaultProtocolCharset
+     */
+    public String getProtocolCharset() {
+        return (protocolCharset != null)
+                ? protocolCharset
+                : defaultProtocolCharset;
+    }
+
+    /**
+     * Set the default charset of the document.
+     * <p>
+     * Notice that it will be possible to contain mixed characters (e.g.
+     * ftp://host/KoreanNamespace/ChineseResource). To handle the Bi-directional
+     * display of these character sets, the protocol charset could be simply
+     * used again. Because it's not yet implemented that the insertion of BIDI
+     * control characters at different points during composition is extracted.
+     * <p>
+     * Always all the time, the setter method is always succeeded and throws
+     * <code>DefaultCharsetChanged</code> exception. So API programmer must
+     * follow the following way: <code><pre>
+     *  import org.apache.util.URI$DefaultCharsetChanged;
+     *      .
+     *      .
+     *      .
+     *  try {
+     *      URI.setDefaultDocumentCharset("EUC-KR");
+     *  } catch (DefaultCharsetChanged cc) {
+     *      // CASE 1: the exception could be ignored, when it is set by user
+     *      if (cc.getReasonCode() == DefaultCharsetChanged.DOCUMENT_CHARSET) {
+     *      // CASE 2: let user know the default document charset changed
+     *      } else {
+     *      // CASE 2: let user know the default protocol charset changed
+     *      }
+     *  }
+     *  </pre></code> The API programmer is responsible to set the correct
+     * charset. And each application should remember its own charset to support.
+     *
+     * @param charset the default charset for the document
+     * @throws DefaultCharsetChanged default charset changed
+     */
+    public static void setDefaultDocumentCharset(String charset)
+            throws DefaultCharsetChanged {
+
+        defaultDocumentCharset = charset;
+        throw new DefaultCharsetChanged(DefaultCharsetChanged.DOCUMENT_CHARSET,
+            "the default document charset changed");
+    }
+
+    /**
+     * Get the recommended default charset of the document.
+     *
+     * @return the default charset string
+     */
+    public static String getDefaultDocumentCharset() {
+        return defaultDocumentCharset;
+    }
+
+    /**
+     * Get the default charset of the document by locale.
+     *
+     * @return the default charset string by locale
+     */
+    public static String getDefaultDocumentCharsetByLocale() {
+        return defaultDocumentCharsetByLocale;
+    }
+
+    /**
+     * Get the default charset of the document by platform.
+     *
+     * @return the default charset string by platform
+     */
+    public static String getDefaultDocumentCharsetByPlatform() {
+        return defaultDocumentCharsetByPlatform;
+    }
+
+    // ------------------------------------------------------------- The scheme
+
+    /**
+     * Get the scheme.
+     *
+     * @return the scheme
+     */
+    public char[] getRawScheme() {
+        return _scheme;
+    }
+
+    /**
+     * Get the scheme.
+     *
+     * @return the scheme null if undefined scheme
+     */
+    public String getScheme() {
+        return (_scheme == null) ? null : new String(_scheme);
+    }
+
+    // ---------------------------------------------------------- The authority
+
+    /**
+     * Set the authority. It can be one type of server, hostport, hostname,
+     * IPv4address, IPv6reference and reg_name.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * authority = server | reg_name
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     *
+     * @param escapedAuthority the raw escaped authority
+     * @throws URIException If {@link #parseAuthority(java.lang.String,boolean)}
+     *             fails
+     * @throws NullPointerException null authority
+     */
+    public void setRawAuthority(char[] escapedAuthority) throws URIException,
+            NullPointerException {
+
+        parseAuthority(new String(escapedAuthority), true);
+        setURI();
+    }
+
+    /**
+     * Set the authority. It can be one type of server, hostport, hostname,
+     * IPv4address, IPv6reference and reg_name. Note that there is no
+     * setAuthority method by the escape encoding reason.
+     *
+     * @param escapedAuthority the escaped authority string
+     * @throws URIException If {@link #parseAuthority(java.lang.String,boolean)}
+     *             fails
+     */
+    public void setEscapedAuthority(String escapedAuthority)
+            throws URIException {
+
+        parseAuthority(escapedAuthority, true);
+        setURI();
+    }
+
+    /**
+     * Get the raw-escaped authority.
+     *
+     * @return the raw-escaped authority
+     */
+    public char[] getRawAuthority() {
+        return _authority;
+    }
+
+    /**
+     * Get the escaped authority.
+     *
+     * @return the escaped authority
+     */
+    public String getEscapedAuthority() {
+        return (_authority == null) ? null : new String(_authority);
+    }
+
+    /**
+     * Get the authority.
+     *
+     * @return the authority
+     * @throws URIException If {@link #decode} fails
+     */
+    public String getAuthority() throws URIException {
+        return (_authority == null) ? null : decode(_authority,
+            getProtocolCharset());
+    }
+
+    // ----------------------------------------------------------- The userinfo
+
+    /**
+     * Get the raw-escaped userinfo.
+     *
+     * @return the raw-escaped userinfo
+     * @see #getAuthority
+     */
+    public char[] getRawUserinfo() {
+        return _userinfo;
+    }
+
+    /**
+     * Get the escaped userinfo.
+     *
+     * @return the escaped userinfo
+     * @see #getAuthority
+     */
+    public String getEscapedUserinfo() {
+        return (_userinfo == null) ? null : new String(_userinfo);
+    }
+
+    /**
+     * Get the userinfo.
+     *
+     * @return the userinfo
+     * @throws URIException If {@link #decode} fails
+     * @see #getAuthority
+     */
+    public String getUserinfo() throws URIException {
+        return (_userinfo == null) ? null : decode(_userinfo,
+            getProtocolCharset());
+    }
+
+    // --------------------------------------------------------------- The host
+
+    /**
+     * Get the host.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * host = hostname | IPv4address | IPv6reference
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     *
+     * @return the host
+     * @see #getAuthority
+     */
+    public char[] getRawHost() {
+        return _host;
+    }
+
+    /**
+     * Get the host.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     * host = hostname | IPv4address | IPv6reference
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     *
+     * @return the host
+     * @throws URIException If {@link #decode} fails
+     * @see #getAuthority
+     */
+    public String getHost() throws URIException {
+        if (_host != null) {
+            return decode(_host, getProtocolCharset());
+        }
+        return null;
+    }
+
+    // --------------------------------------------------------------- The port
+
+    /**
+     * Get the port. In order to get the specfic default port, the specific
+     * protocol-supported class extended from the URI class should be used. It
+     * has the server-based naming authority.
+     *
+     * @return the port if -1, it has the default port for the scheme or the
+     *         server-based naming authority is not supported in the specific
+     *         URI.
+     */
+    public int getPort() {
+        return _port;
+    }
+
+    // --------------------------------------------------------------- The path
+
+    /**
+     * Set the raw-escaped path.
+     *
+     * @param escapedPath the path character sequence
+     * @throws URIException encoding error or not proper for initial instance
+     * @see #encode
+     */
+    public void setRawPath(char[] escapedPath) throws URIException {
+        if (escapedPath == null || escapedPath.length == 0) {
+            _path = _opaque = escapedPath;
+            setURI();
+            return;
+        }
+        // remove the fragment identifier
+        escapedPath = removeFragmentIdentifier(escapedPath);
+        if (_is_net_path || _is_abs_path) {
+            if (escapedPath[0] != '/') {
+                throw new URIException(URIException.PARSING,
+                    "not absolute path");
+            }
+            if (!validate(escapedPath, abs_path)) {
+                throw new URIException(URIException.ESCAPING,
+                    "escaped absolute path not valid");
+            }
+            _path = escapedPath;
+        } else if (_is_rel_path) {
+            int at = indexFirstOf(escapedPath, '/');
+            if (at == 0) {
+                throw new URIException(URIException.PARSING, "incorrect path");
+            }
+            if (at > 0 && !validate(escapedPath, 0, at - 1, rel_segment)
+                && !validate(escapedPath, at, -1, abs_path) || at < 0
+                && !validate(escapedPath, 0, -1, rel_segment)) {
+
+                throw new URIException(URIException.ESCAPING,
+                    "escaped relative path not valid");
+            }
+            _path = escapedPath;
+        } else if (_is_opaque_part) {
+            if (!uric_no_slash.get(escapedPath[0])
+                && !validate(escapedPath, 1, -1, uric)) {
+                throw new URIException(URIException.ESCAPING,
+                    "escaped opaque part not valid");
+            }
+            _opaque = escapedPath;
+        } else {
+            throw new URIException(URIException.PARSING, "incorrect path");
+        }
+        setURI();
+    }
+
+    /**
+     * Set the escaped path.
+     *
+     * @param escapedPath the escaped path string
+     * @throws URIException encoding error or not proper for initial instance
+     * @see #encode
+     */
+    public void setEscapedPath(String escapedPath) throws URIException {
+        if (escapedPath == null) {
+            _path = _opaque = null;
+            setURI();
+            return;
+        }
+        setRawPath(escapedPath.toCharArray());
+    }
+
+    /**
+     * Set the path.
+     *
+     * @param path the path string
+     * @throws URIException set incorrectly or fragment only
+     * @see #encode
+     */
+    public void setPath(String path) throws URIException {
+
+        if (path == null || path.length() == 0) {
+            _path = _opaque = (path == null) ? null : path.toCharArray();
+            setURI();
+            return;
+        }
+        // set the charset to do escape encoding
+        String charset = getProtocolCharset();
+
+        if (_is_net_path || _is_abs_path) {
+            _path = encode(path, allowed_abs_path, charset);
+        } else if (_is_rel_path) {
+            StringBuilder buff = new StringBuilder(path.length());
+            int at = path.indexOf('/');
+            if (at == 0) { // never 0
+                throw new URIException(URIException.PARSING,
+                    "incorrect relative path");
+            }
+            if (at > 0) {
+                buff.append(encode(path.substring(0, at), allowed_rel_path,
+                    charset));
+                buff.append(encode(path.substring(at), allowed_abs_path,
+                    charset));
+            } else {
+                buff.append(encode(path, allowed_rel_path, charset));
+            }
+            _path = buff.toString().toCharArray();
+        } else if (_is_opaque_part) {
+            StringBuilder buf = new StringBuilder();
+            buf.insert(0, encode(path.substring(0, 1), uric_no_slash, charset));
+            buf.insert(1, encode(path.substring(1), uric, charset));
+            _opaque = buf.toString().toCharArray();
+        } else {
+            throw new URIException(URIException.PARSING, "incorrect path");
+        }
+        setURI();
+    }
+
+    /**
+     * Resolve the base and relative path.
+     *
+     * @param basePath a character array of the basePath
+     * @param relPath a character array of the relPath
+     * @return the resolved path
+     * @throws URIException no more higher path level to be resolved
+     */
+    protected char[] resolvePath(char[] basePath, char[] relPath)
+            throws URIException {
+
+        // REMINDME: paths are never null
+        String base = (basePath == null) ? "" : new String(basePath);
+
+        // _path could be empty
+        if (relPath == null || relPath.length == 0) {
+            return normalize(basePath);
+        } else if (relPath[0] == '/') {
+            return normalize(relPath);
+        } else {
+            int at = base.lastIndexOf('/');
+            if (at != -1) {
+                basePath = base.substring(0, at + 1).toCharArray();
+            }
+            StringBuilder buff = new StringBuilder(base.length() + relPath.length);
+            buff.append((at != -1) ? base.substring(0, at + 1) : "/");
+            buff.append(relPath);
+            return normalize(buff.toString().toCharArray());
+        }
+    }
+
+    /**
+     * Get the raw-escaped current hierarchy level in the given path. If the
+     * last namespace is a collection, the slash mark ('/') should be ended with
+     * at the last character of the path string.
+     *
+     * @param path the path
+     * @return the current hierarchy level
+     * @throws URIException no hierarchy level
+     */
+    protected char[] getRawCurrentHierPath(char[] path) throws URIException {
+
+        if (_is_opaque_part) {
+            throw new URIException(URIException.PARSING, "no hierarchy level");
+        }
+        if (path == null) {
+            throw new URIException(URIException.PARSING, "empty path");
+        }
+        String buff = new String(path);
+        int first = buff.indexOf('/');
+        int last = buff.lastIndexOf('/');
+        if (last == 0) {
+            return rootPath;
+        } else if (first != last && last != -1) {
+            return buff.substring(0, last).toCharArray();
+        }
+        // FIXME: it could be a document on the server side
+        return path;
+    }
+
+    /**
+     * Get the raw-escaped current hierarchy level.
+     *
+     * @return the raw-escaped current hierarchy level
+     * @throws URIException If {@link #getRawCurrentHierPath(char[])} fails.
+     */
+    public char[] getRawCurrentHierPath() throws URIException {
+        return (_path == null) ? null : getRawCurrentHierPath(_path);
+    }
+
+    /**
+     * Get the escaped current hierarchy level.
+     *
+     * @return the escaped current hierarchy level
+     * @throws URIException If {@link #getRawCurrentHierPath(char[])} fails.
+     */
+    public String getEscapedCurrentHierPath() throws URIException {
+        char[] path = getRawCurrentHierPath();
+        return (path == null) ? null : new String(path);
+    }
+
+    /**
+     * Get the current hierarchy level.
+     *
+     * @return the current hierarchy level
+     * @throws URIException If {@link #getRawCurrentHierPath(char[])} fails.
+     * @see #decode
+     */
+    public String getCurrentHierPath() throws URIException {
+        char[] path = getRawCurrentHierPath();
+        return (path == null) ? null : decode(path, getProtocolCharset());
+    }
+
+    /**
+     * Get the level above the this hierarchy level.
+     *
+     * @return the raw above hierarchy level
+     * @throws URIException If {@link #getRawCurrentHierPath(char[])} fails.
+     */
+    public char[] getRawAboveHierPath() throws URIException {
+        char[] path = getRawCurrentHierPath();
+        return (path == null) ? null : getRawCurrentHierPath(path);
+    }
+
+    /**
+     * Get the level above the this hierarchy level.
+     *
+     * @return the raw above hierarchy level
+     * @throws URIException If {@link #getRawCurrentHierPath(char[])} fails.
+     */
+    public String getEscapedAboveHierPath() throws URIException {
+        char[] path = getRawAboveHierPath();
+        return (path == null) ? null : new String(path);
+    }
+
+    /**
+     * Get the level above the this hierarchy level.
+     *
+     * @return the above hierarchy level
+     * @throws URIException If {@link #getRawCurrentHierPath(char[])} fails.
+     * @see #decode
+     */
+    public String getAboveHierPath() throws URIException {
+        char[] path = getRawAboveHierPath();
+        return (path == null) ? null : decode(path, getProtocolCharset());
+    }
+
+    /**
+     * Get the raw-escaped path.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     *   path          = [ abs_path | opaque_part ]
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     *
+     * @return the raw-escaped path
+     */
+    public char[] getRawPath() {
+        return _is_opaque_part ? _opaque : _path;
+    }
+
+    /**
+     * Get the escaped path.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     *   path          = [ abs_path | opaque_part ]
+     *   abs_path      = "/"  path_segments
+     *   opaque_part   = uric_no_slash *uric
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     *
+     * @return the escaped path string
+     */
+    public String getEscapedPath() {
+        char[] path = getRawPath();
+        return (path == null) ? null : new String(path);
+    }
+
+    /**
+     * Get the path.
+     * <p>
+     * <blockquote>
+     *
+     * <pre>
+     *   path          = [ abs_path | opaque_part ]
+     * </pre>
+     *
+     * </blockquote>
+     * <p>
+     *
+     * @return the path string
+     * @throws URIException If {@link #decode} fails.
+     * @see #decode
+     */
+    public String getPath() throws URIException {
+        char[] path = getRawPath();
+        return (path == null) ? null : decode(path, getProtocolCharset());
+    }
+
+    /**
+     * Get the raw-escaped basename of the path.
+     *
+     * @return the raw-escaped basename
+     */
+    public char[] getRawName() {
+        if (_path == null) {
+            return null;
+        }
+
+        int at = 0;
+        for (int i = _path.length - 1; i >= 0; i--) {
+            if (_path[i] == '/') {
+                at = i + 1;
+                break;
+            }
+        }
+        int len = _path.length - at;
+        char[] basename = new char[len];
+        System.arraycopy(_path, at, basename, 0, len);
+        return basename;
+    }
+
+    /**
+     * Get the escaped basename of the path.
+     *
+     * @return the escaped basename string
+     */
+    public String getEscapedName() {
+        char[] basename = getRawName();
+        return (basename == null) ? null : new String(basename);
+    }
+
+    /**
+     * Get the basename of the path.
+     *
+     * @return the basename string
+     * @throws URIException incomplete trailing escape pattern or unsupported
+     *             character encoding
+     * @see #decode
+     */
+    public String getName() throws URIException {
+        char[] basename = getRawName();
+        return (basename == null) ? null : decode(getRawName(),
+            getProtocolCharset());
+    }
+
+    // ----------------------------------------------------- The path and query
+
+    /**
+     * Get the raw-escaped path and query.
+     *
+     * @return the raw-escaped path and query
+     */
+    public char[] getRawPathQuery() {
+
+        if (_path == null && _query == null) {
+            return null;
+        }
+        StringBuilder buff = new StringBuilder();
+        if (_path != null) {
+            buff.append(_path);
+        }
+        if (_query != null) {
+            buff.append('?');
+            buff.append(_query);
+        }
+        return buff.toString().toCharArray();
+    }
+
+    /**
+     * Get the escaped query.
+     *
+     * @return the escaped path and query string
+     */
+    public String getEscapedPathQuery() {
+        char[] rawPathQuery = getRawPathQuery();
+        return (rawPathQuery == null) ? null : new String(rawPathQuery);
+    }
+
+    /**
+     * Get the path and query.
+     *
+     * @return the path and query string.
+     * @throws URIException incomplete trailing escape pattern or unsupported
+     *             character encoding
+     * @see #decode
+     */
+    public String getPathQuery() throws URIException {
+        char[] rawPathQuery = getRawPathQuery();
+        return (rawPathQuery == null) ? null : decode(rawPathQuery,
+            getProtocolCharset());
+    }
+
+    // -------------------------------------------------------------- The query
+
+    /**
+     * Set the raw-escaped query.
+     *
+     * @param escapedQuery the raw-escaped query
+     * @throws URIException escaped query not valid
+     */
+    public void setRawQuery(char[] escapedQuery) throws URIException {
+        if (escapedQuery == null || escapedQuery.length == 0) {
+            _query = escapedQuery;
+            setURI();
+            return;
+        }
+        // remove the fragment identifier
+        escapedQuery = removeFragmentIdentifier(escapedQuery);
+        if (!validate(escapedQuery, query)) {
+            throw new URIException(URIException.ESCAPING,
+                "escaped query not valid");
+        }
+        _query = escapedQuery;
+        setURI();
+    }
+
+    /**
+     * Set the escaped query string.
+     *
+     * @param escapedQuery the escaped query string
+     * @throws URIException escaped query not valid
+     */
+    public void setEscapedQuery(String escapedQuery) throws URIException {
+        if (escapedQuery == null) {
+            _query = null;
+            setURI();
+            return;
+        }
+        setRawQuery(escapedQuery.toCharArray());
+    }
+
+    /**
+     * Set the query.
+     * <p>
+     * When a query string is not misunderstood the reserved special characters
+     * ("&amp;", "=", "+", ",", and "$") within a query component, it is
+     * recommended to use in encoding the whole query with this method.
+     * <p>
+     * The additional APIs for the special purpose using by the reserved special
+     * characters used in each protocol are implemented in each protocol classes
+     * inherited from <code>URI</code>. So refer to the same-named APIs
+     * implemented in each specific protocol instance.
+     *
+     * @param query the query string.
+     * @throws URIException incomplete trailing escape pattern or unsupported
+     *             character encoding
+     * @see #encode
+     */
+    public void setQuery(String query) throws URIException {
+        if (query == null || query.length() == 0) {
+            _query = (query == null) ? null : query.toCharArray();
+            setURI();
+            return;
+        }
+        setRawQuery(encode(query, allowed_query, getProtocolCharset()));
+    }
+
+    /**
+     * Get the raw-escaped query.
+     *
+     * @return the raw-escaped query
+     */
+    public char[] getRawQuery() {
+        return _query;
+    }
+
+    /**
+     * Get the escaped query.
+     *
+     * @return the escaped query string
+     */
+    public String getEscapedQuery() {
+        return (_query == null) ? null : new String(_query);
+    }
+
+    /**
+     * Get the query.
+     *
+     * @return the query string.
+     * @throws URIException incomplete trailing escape pattern or unsupported
+     *             character encoding
+     * @see #decode
+     */
+    public String getQuery() throws URIException {
+        return (_query == null) ? null : decode(_query, getProtocolCharset());
+    }
+
+    // ----------------------------------------------------------- The fragment
+
+    /**
+     * Set the raw-escaped fragment.
+     *
+     * @param escapedFragment the raw-escaped fragment
+     * @throws URIException escaped fragment not valid
+     */
+    public void setRawFragment(char[] escapedFragment) throws URIException {
+        if (escapedFragment == null || escapedFragment.length == 0) {
+            _fragment = escapedFragment;
+            hash = 0;
+            return;
+        }
+        if (!validate(escapedFragment, fragment)) {
+            throw new URIException(URIException.ESCAPING,
+                "escaped fragment not valid");
+        }
+        _fragment = escapedFragment;
+        hash = 0;
+    }
+
+    /**
+     * Set the escaped fragment string.
+     *
+     * @param escapedFragment the escaped fragment string
+     * @throws URIException escaped fragment not valid
+     */
+    public void setEscapedFragment(String escapedFragment) throws URIException {
+        if (escapedFragment == null) {
+            _fragment = null;
+            hash = 0;
+            return;
+        }
+        setRawFragment(escapedFragment.toCharArray());
+    }
+
+    /**
+     * Set the fragment.
+     *
+     * @param fragment the fragment string.
+     * @throws URIException If an error occurs.
+     */
+    public void setFragment(String fragment) throws URIException {
+        if (fragment == null || fragment.length() == 0) {
+            _fragment = (fragment == null) ? null : fragment.toCharArray();
+            hash = 0;
+            return;
+        }
+        _fragment = encode(fragment, allowed_fragment, getProtocolCharset());
+        hash = 0;
+    }
+
+    /**
+     * Get the raw-escaped fragment.
+     * <p>
+     * The optional fragment identifier is not part of a URI, but is often used
+     * in conjunction with a URI.
+     * <p>
+     * The format and interpretation of fragment identifiers is dependent on the
+     * media type [RFC2046] of the retrieval result.
+     * <p>
+     * A fragment identifier is only meaningful when a URI reference is intended
+     * for retrieval and the result of that retrieval is a document for which
+     * the identified fragment is consistently defined.
+     *
+     * @return the raw-escaped fragment
+     */
+    public char[] getRawFragment() {
+        return _fragment;
+    }
+
+    /**
+     * Get the escaped fragment.
+     *
+     * @return the escaped fragment string
+     */
+    public String getEscapedFragment() {
+        return (_fragment == null) ? null : new String(_fragment);
+    }
+
+    /**
+     * Get the fragment.
+     *
+     * @return the fragment string
+     * @throws URIException incomplete trailing escape pattern or unsupported
+     *             character encoding
+     * @see #decode
+     */
+    public String getFragment() throws URIException {
+        return (_fragment == null) ? null : decode(_fragment,
+            getProtocolCharset());
+    }
+
+    // ------------------------------------------------------------- Utilities
+
+    /**
+     * Remove the fragment identifier of the given component.
+     *
+     * @param component the component that a fragment may be included
+     * @return the component that the fragment identifier is removed
+     */
+    protected char[] removeFragmentIdentifier(char[] component) {
+        if (component == null) {
+            return null;
+        }
+        int lastIndex = new String(component).indexOf('#');
+        if (lastIndex != -1) {
+            component = new String(component).substring(0, lastIndex).toCharArray();
+        }
+        return component;
+    }
+
+    /**
+     * Normalize the given hier path part.
+     * <p>
+     * Algorithm taken from URI reference parser at
+     * http://www.apache.org/~fielding/uri/rev-2002/issues.html.
+     *
+     * @param path the path to normalize
+     * @return the normalized path
+     * @throws URIException no more higher path level to be normalized
+     */
+    protected char[] normalize(char[] path) throws URIException {
+
+        if (path == null) {
+            return null;
+        }
+
+        String normalized = new String(path);
+
+        // If the buffer begins with "./" or "../", the "." or ".." is removed.
+        if (normalized.startsWith("./")) {
+            normalized = normalized.substring(1);
+        } else if (normalized.startsWith("../")) {
+            normalized = normalized.substring(2);
+        } else if (normalized.startsWith("..")) {
+            normalized = normalized.substring(2);
+        }
+
+        // All occurrences of "/./" in the buffer are replaced with "/"
+        int index = -1;
+        while ((index = normalized.indexOf("/./")) != -1) {
+            normalized = normalized.substring(0, index)
+                + normalized.substring(index + 2);
+        }
+
+        // If the buffer ends with "/.", the "." is removed.
+        if (normalized.endsWith("/.")) {
+            normalized = normalized.substring(0, normalized.length() - 1);
+        }
+
+        int startIndex = 0;
+
+        // All occurrences of "/<segment>/../" in the buffer, where ".."
+        // and <segment> are complete path segments, are iteratively replaced
+        // with "/" in order from left to right until no matching pattern
+        // remains.
+        // If the buffer ends with "/<segment>/..", that is also replaced
+        // with "/". Note that <segment> may be empty.
+        while ((index = normalized.indexOf("/../", startIndex)) != -1) {
+            int slashIndex = normalized.lastIndexOf('/', index - 1);
+            if (slashIndex >= 0) {
+                normalized = normalized.substring(0, slashIndex)
+                    + normalized.substring(index + 3);
+            } else {
+                startIndex = index + 3;
+            }
+        }
+        if (normalized.endsWith("/..")) {
+            int slashIndex = normalized.lastIndexOf('/',
+                normalized.length() - 4);
+            if (slashIndex >= 0) {
+                normalized = normalized.substring(0, slashIndex + 1);
+            }
+        }
+
+        // All prefixes of "<segment>/../" in the buffer, where ".."
+        // and <segment> are complete path segments, are iteratively replaced
+        // with "/" in order from left to right until no matching pattern
+        // remains.
+        // If the buffer ends with "<segment>/..", that is also replaced
+        // with "/". Note that <segment> may be empty.
+        while ((index = normalized.indexOf("/../")) != -1) {
+            int slashIndex = normalized.lastIndexOf('/', index - 1);
+            if (slashIndex >= 0) {
+                break;
+            }
+            normalized = normalized.substring(index + 3);
+        }
+        if (normalized.endsWith("/..")) {
+            int slashIndex = normalized.lastIndexOf('/',
+                normalized.length() - 4);
+            if (slashIndex < 0) {
+                normalized = "/";
+            }
+        }
+
+        return normalized.toCharArray();
+    }
+
+    /**
+     * Normalizes the path part of this URI. Normalization is only meant to be
+     * performed on URIs with an absolute path. Calling this method on a
+     * relative path URI will have no effect.
+     *
+     * @throws URIException no more higher path level to be normalized
+     * @see #isAbsPath()
+     */
+    public void normalize() throws URIException {
+        if (isAbsPath()) {
+            _path = normalize(_path);
+            setURI();
+        }
+    }
+
+    /**
+     * Test if the first array is equal to the second array.
+     *
+     * @param first the first character array
+     * @param second the second character array
+     * @return true if they're equal
+     */
+    protected boolean equals(char[] first, char[] second) {
+
+        if (first == null && second == null) {
+            return true;
+        }
+        if (first == null || second == null) {
+            return false;
+        }
+        if (first.length != second.length) {
+            return false;
+        }
+        for (int i = 0; i < first.length; i++) {
+            if (first[i] != second[i]) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Test an object if this URI is equal to another.
+     *
+     * @param obj an object to compare
+     * @return true if two URI objects are equal
+     */
+    public boolean equals(Object obj) {
+
+        // normalize and test each components
+        if (obj == this) {
+            return true;
+        }
+        if (!(obj instanceof URI)) {
+            return false;
+        }
+        URI another = (URI) obj;
+        // scheme
+        if (!equals(_scheme, another._scheme)) {
+            return false;
+        }
+        // is_opaque_part or is_hier_part? and opaque
+        if (!equals(_opaque, another._opaque)) {
+            return false;
+        }
+        // is_hier_part
+        // has_authority
+        if (!equals(_authority, another._authority)) {
+            return false;
+        }
+        // path
+        if (!equals(_path, another._path)) {
+            return false;
+        }
+        // has_query
+        if (!equals(_query, another._query)) {
+            return false;
+        }
+        // has_fragment? should be careful of the only fragment case.
+        if (!equals(_fragment, another._fragment)) {
+            return false;
+        }
+        return true;
+    }
+
+    // ---------------------------------------------------------- Serialization
+
+    /**
+     * Write the content of this URI.
+     *
+     * @param oos the object-output stream
+     * @throws IOException If an IO problem occurs.
+     */
+    private void writeObject(ObjectOutputStream oos) throws IOException {
+
+        oos.defaultWriteObject();
+    }
+
+    /**
+     * Read a URI.
+     *
+     * @param ois the object-input stream
+     * @throws ClassNotFoundException If one of the classes specified in the
+     *             input stream cannot be found.
+     * @throws IOException If an IO problem occurs.
+     */
+    private void readObject(ObjectInputStream ois)
+            throws ClassNotFoundException, IOException {
+
+        ois.defaultReadObject();
+    }
+
+    // -------------------------------------------------------------- Hash code
+
+    /**
+     * Return a hash code for this URI.
+     *
+     * @return a has code value for this URI
+     */
+    public int hashCode() {
+        if (hash == 0) {
+            char[] c = _uri;
+            if (c != null) {
+                for (int i = 0, len = c.length; i < len; i++) {
+                    hash = 31 * hash + c[i];
+                }
+            }
+            c = _fragment;
+            if (c != null) {
+                for (int i = 0, len = c.length; i < len; i++) {
+                    hash = 31 * hash + c[i];
+                }
+            }
+        }
+        return hash;
+    }
+
+    // ------------------------------------------------------------- Comparison
+
+    /**
+     * Compare this URI to another object.
+     *
+     * @param another the object to be compared.
+     * @return 0, if it's same, -1, if failed, first being compared with in the
+     *         authority component
+     * @throws ClassCastException not URI argument
+     */
+    public int compareTo(URI another) {
+
+        if (!equals(_authority, another.getRawAuthority())) {
+            return -1;
+        }
+        return toString().compareTo(another.toString());
+    }
+
+    // ------------------------------------------------------------------ Clone
+
+    /**
+     * Create and return a copy of this object, the URI-reference containing the
+     * userinfo component. Notice that the whole URI-reference including the
+     * userinfo component counld not be gotten as a <code>String</code>.
+     * <p>
+     * To copy the identical <code>URI</code> object including the userinfo
+     * component, it should be used.
+     *
+     * @return a clone of this instance
+     */
+    public synchronized Object clone() throws CloneNotSupportedException {
+
+        URI instance = (URI) super.clone();
+
+        instance._uri = _uri;
+        instance._scheme = _scheme;
+        instance._opaque = _opaque;
+        instance._authority = _authority;
+        instance._userinfo = _userinfo;
+        instance._host = _host;
+        instance._port = _port;
+        instance._path = _path;
+        instance._query = _query;
+        instance._fragment = _fragment;
+        // the charset to do escape encoding for this instance
+        instance.protocolCharset = protocolCharset;
+        // flags
+        instance._is_hier_part = _is_hier_part;
+        instance._is_opaque_part = _is_opaque_part;
+        instance._is_net_path = _is_net_path;
+        instance._is_abs_path = _is_abs_path;
+        instance._is_rel_path = _is_rel_path;
+        instance._is_reg_name = _is_reg_name;
+        instance._is_server = _is_server;
+        instance._is_hostname = _is_hostname;
+        instance._is_IPv4address = _is_IPv4address;
+        instance._is_IPv6reference = _is_IPv6reference;
+
+        return instance;
+    }
+
+    // ------------------------------------------------------------ Get the URI
+
+    /**
+     * It can be gotten the URI character sequence. It's raw-escaped. For the
+     * purpose of the protocol to be transported, it will be useful.
+     * <p>
+     * It is clearly unwise to use a URL that contains a password which is
+     * intended to be secret. In particular, the use of a password within the
+     * 'userinfo' component of a URL is strongly disrecommended except in those
+     * rare cases where the 'password' parameter is intended to be public.
+     * <p>
+     * When you want to get each part of the userinfo, you need to use the
+     * specific methods in the specific URL. It depends on the specific URL.
+     *
+     * @return the URI character sequence
+     */
+    public char[] getRawURI() {
+        return _uri;
+    }
+
+    /**
+     * It can be gotten the URI character sequence. It's escaped. For the
+     * purpose of the protocol to be transported, it will be useful.
+     *
+     * @return the escaped URI string
+     */
+    public String getEscapedURI() {
+        return (_uri == null) ? null : new String(_uri);
+    }
+
+    /**
+     * It can be gotten the URI character sequence.
+     *
+     * @return the original URI string
+     * @throws URIException incomplete trailing escape pattern or unsupported
+     *             character encoding
+     * @see #decode
+     */
+    public String getURI() throws URIException {
+        return (_uri == null) ? null : decode(_uri, getProtocolCharset());
+    }
+
+    /**
+     * Get the URI reference character sequence.
+     *
+     * @return the URI reference character sequence
+     */
+    public char[] getRawURIReference() {
+        if (_fragment == null) {
+            return _uri;
+        }
+        if (_uri == null) {
+            return _fragment;
+        }
+        // if _uri != null && _fragment != null
+        String uriReference = new String(_uri) + "#" + new String(_fragment);
+        return uriReference.toCharArray();
+    }
+
+    /**
+     * Get the escaped URI reference string.
+     *
+     * @return the escaped URI reference string
+     */
+    public String getEscapedURIReference() {
+        char[] uriReference = getRawURIReference();
+        return (uriReference == null) ? null : new String(uriReference);
+    }
+
+    /**
+     * Get the original URI reference string.
+     *
+     * @return the original URI reference string
+     * @throws URIException If {@link #decode} fails.
+     */
+    public String getURIReference() throws URIException {
+        char[] uriReference = getRawURIReference();
+        return (uriReference == null) ? null : decode(uriReference,
+            getProtocolCharset());
+    }
+
+    /**
+     * Get the escaped URI string.
+     * <p>
+     * On the document, the URI-reference form is only used without the userinfo
+     * component like http://jakarta.apache.org/ by the security reason. But the
+     * URI-reference form with the userinfo component could be parsed.
+     * <p>
+     * In other words, this URI and any its subclasses must not expose the
+     * URI-reference expression with the userinfo component like
+     * http://user:password@hostport/restricted_zone.<br>
+     * It means that the API client programmer should extract each user and
+     * password to access manually. Probably it will be supported in the each
+     * subclass, however, not a whole URI-reference expression.
+     *
+     * @return the escaped URI string
+     * @see #clone()
+     */
+    public String toString() {
+        return getEscapedURI();
+    }
+
+    // ------------------------------------------------------------ Inner class
+
+    /**
+     * The charset-changed normal operation to represent to be required to alert
+     * to user the fact the default charset is changed.
+     */
+    @SuppressWarnings("serial")
+    public static class DefaultCharsetChanged extends SlingException {
+
+        // ------------------------------------------------------- constructors
+
+        /**
+         * The constructor with a reason string and its code arguments.
+         *
+         * @param reasonCode the reason code
+         * @param reason the reason
+         */
+        public DefaultCharsetChanged(int reasonCode, String reason) {
+            super(reason);
+            this.reason = reason;
+            this.reasonCode = reasonCode;
+        }
+
+        // ---------------------------------------------------------- constants
+
+        /** No specified reason code. */
+        public static final int UNKNOWN = 0;
+
+        /** Protocol charset changed. */
+        public static final int PROTOCOL_CHARSET = 1;
+
+        /** Document charset changed. */
+        public static final int DOCUMENT_CHARSET = 2;
+
+        // ------------------------------------------------- instance variables
+
+        /** The reason code. */
+        private int reasonCode;
+
+        /** The reason message. */
+        private String reason;
+
+        // ------------------------------------------------------------ methods
+
+        /**
+         * Get the reason code.
+         *
+         * @return the reason code
+         */
+        public int getReasonCode() {
+            return reasonCode;
+        }
+
+        /**
+         * Get the reason message.
+         *
+         * @return the reason message
+         */
+        public String getReason() {
+            return reason;
+        }
+
+    }
+
+    /**
+     * A mapping to determine the (somewhat arbitrarily) preferred charset for a
+     * given locale. Supports all locales recognized in JDK 1.1.
+     * <p>
+     * The distribution of this class is Servlets.com. It was originally written
+     * by Jason Hunter [jhunter at acm.org] and used by with permission.
+     */
+    public static class LocaleToCharsetMap {
+
+        /** A mapping of language code to charset */
+        private static final HashMap<String, String> LOCALE_TO_CHARSET_MAP;
+        static {
+            LOCALE_TO_CHARSET_MAP = new HashMap<String, String>();
+            LOCALE_TO_CHARSET_MAP.put("ar", "ISO-8859-6");
+            LOCALE_TO_CHARSET_MAP.put("be", "ISO-8859-5");
+            LOCALE_TO_CHARSET_MAP.put("bg", "ISO-8859-5");
+            LOCALE_TO_CHARSET_MAP.put("ca", "ISO-8859-1");
+            LOCALE_TO_CHARSET_MAP.put("cs", "ISO-8859-2");
+            LOCALE_TO_CHARSET_MAP.put("da", "ISO-8859-1");
+            LOCALE_TO_CHARSET_MAP.put("de", "ISO-8859-1");
+            LOCALE_TO_CHARSET_MAP.put("el", "ISO-8859-7");
+            LOCALE_TO_CHARSET_MAP.put("en", "ISO-8859-1");
+            LOCALE_TO_CHARSET_MAP.put("es", "ISO-8859-1");
+            LOCALE_TO_CHARSET_MAP.put("et", "ISO-8859-1");
+            LOCALE_TO_CHARSET_MAP.put("fi", "ISO-8859-1");
+            LOCALE_TO_CHARSET_MAP.put("fr", "ISO-8859-1");
+            LOCALE_TO_CHARSET_MAP.put("hr", "ISO-8859-2");
+            LOCALE_TO_CHARSET_MAP.put("hu", "ISO-8859-2");
+            LOCALE_TO_CHARSET_MAP.put("is", "ISO-8859-1");
+            LOCALE_TO_CHARSET_MAP.put("it", "ISO-8859-1");
+            LOCALE_TO_CHARSET_MAP.put("iw", "ISO-8859-8");
+            LOCALE_TO_CHARSET_MAP.put("ja", "Shift_JIS");
+            LOCALE_TO_CHARSET_MAP.put("ko", "EUC-KR");
+            LOCALE_TO_CHARSET_MAP.put("lt", "ISO-8859-2");
+            LOCALE_TO_CHARSET_MAP.put("lv", "ISO-8859-2");
+            LOCALE_TO_CHARSET_MAP.put("mk", "ISO-8859-5");
+            LOCALE_TO_CHARSET_MAP.put("nl", "ISO-8859-1");
+            LOCALE_TO_CHARSET_MAP.put("no", "ISO-8859-1");
+            LOCALE_TO_CHARSET_MAP.put("pl", "ISO-8859-2");
+            LOCALE_TO_CHARSET_MAP.put("pt", "ISO-8859-1");
+            LOCALE_TO_CHARSET_MAP.put("ro", "ISO-8859-2");
+            LOCALE_TO_CHARSET_MAP.put("ru", "ISO-8859-5");
+            LOCALE_TO_CHARSET_MAP.put("sh", "ISO-8859-5");
+            LOCALE_TO_CHARSET_MAP.put("sk", "ISO-8859-2");
+            LOCALE_TO_CHARSET_MAP.put("sl", "ISO-8859-2");
+            LOCALE_TO_CHARSET_MAP.put("sq", "ISO-8859-2");
+            LOCALE_TO_CHARSET_MAP.put("sr", "ISO-8859-5");
+            LOCALE_TO_CHARSET_MAP.put("sv", "ISO-8859-1");
+            LOCALE_TO_CHARSET_MAP.put("tr", "ISO-8859-9");
+            LOCALE_TO_CHARSET_MAP.put("uk", "ISO-8859-5");
+            LOCALE_TO_CHARSET_MAP.put("zh", "GB2312");
+            LOCALE_TO_CHARSET_MAP.put("zh_TW", "Big5");
+        }
+
+        /**
+         * Get the preferred charset for the given locale.
+         *
+         * @param locale the locale
+         * @return the preferred charset or null if the locale is not
+         *         recognized.
+         */
+        public static String getCharset(Locale locale) {
+            // try for an full name match (may include country)
+            String charset = LOCALE_TO_CHARSET_MAP.get(locale.toString());
+            if (charset != null) {
+                return charset;
+            }
+
+            // if a full name didn't match, try just the language
+            charset = LOCALE_TO_CHARSET_MAP.get(locale.getLanguage());
+            return charset; // may be null
+        }
+
+    }
+
+    // from EncodingUtils...
+
+    /**
+     * Converts the specified string to a byte array. If the charset is not
+     * supported the default system charset is used.
+     *
+     * @param data the string to be encoded
+     * @param charset the desired character encoding
+     * @return The resulting byte array.
+     * @since 3.0
+     */
+    private static byte[] getBytes(final String data, String charset) {
+
+        if (data == null) {
+            throw new IllegalArgumentException("data may not be null");
+        }
+
+        if (charset == null || charset.length() == 0) {
+            throw new IllegalArgumentException(
+                "charset may not be null or empty");
+        }
+
+        try {
+            return data.getBytes(charset);
+        } catch (UnsupportedEncodingException e) {
+
+            // if (LOG.isWarnEnabled()) {
+            // LOG.warn("Unsupported encoding: " + charset +
+            // ". System encoding used.");
+            // }
+
+            return data.getBytes();
+        }
+    }
+
+    /**
+     * Converts the byte array of ASCII characters to a string. This method is
+     * to be used when decoding content of HTTP elements (such as response
+     * headers)
+     *
+     * @param data the byte array to be encoded
+     * @param offset the index of the first byte to encode
+     * @param length the number of bytes to encode
+     * @return The string representation of the byte array
+     * @since 3.0
+     */
+    private static String getAsciiString(final byte[] data) {
+
+        if (data == null) {
+            throw new IllegalArgumentException("Parameter may not be null");
+        }
+
+        try {
+            return new String(data, "US-ASCII");
+        } catch (UnsupportedEncodingException e) {
+            throw new URIException("HttpClient requires ASCII support");
+        }
+    }
+
+    /**
+     * Converts the byte array of HTTP content characters to a string. If the
+     * specified charset is not supported, default system encoding is used.
+     *
+     * @param data the byte array to be encoded
+     * @param charset the desired character encoding
+     * @return The result of the conversion.
+     * @since 3.0
+     */
+    public static String getString(final byte[] data, String charset) {
+
+        if (data == null) {
+            throw new IllegalArgumentException("Parameter may not be null");
+        }
+
+        if (charset == null || charset.length() == 0) {
+            throw new IllegalArgumentException(
+                "charset may not be null or empty");
+        }
+
+        try {
+            return new String(data, charset);
+        } catch (UnsupportedEncodingException e) {
+
+            // if (LOG.isWarnEnabled()) {
+            // LOG.warn("Unsupported encoding: " + charset +
+            // ". System encoding used");
+            // }
+            return new String(data);
+        }
+    }
+
+    /**
+     * Converts the specified string to byte array of ASCII characters.
+     *
+     * @param data the string to be encoded
+     * @return The string as a byte array.
+     * @since 3.0
+     */
+    public static byte[] getAsciiBytes(final String data) {
+
+        if (data == null) {
+            throw new IllegalArgumentException("Parameter may not be null");
+        }
+
+        try {
+            return data.getBytes("US-ASCII");
+        } catch (UnsupportedEncodingException e) {
+            throw new URIException("HttpClient requires ASCII support");
+        }
+    }
+
+    /**
+     * Encodes an array of bytes into an array of URL safe 7-bit characters.
+     * Unsafe characters are escaped.
+     *
+     * @param urlsafe bitset of characters deemed URL safe
+     * @param bytes array of bytes to convert to URL safe characters
+     * @return array of bytes containing URL safe characters
+     */
+    private static final byte[] encodeUrl(BitSet urlsafe, byte[] bytes) {
+        if (bytes == null) {
+            return null;
+        }
+
+        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+        for (int i = 0; i < bytes.length; i++) {
+            int b = bytes[i];
+            if (b < 0) {
+                b = 256 + b;
+            }
+            if (urlsafe.get(b)) {
+                if (b == ' ') {
+                    b = '+';
+                }
+                buffer.write(b);
+            } else {
+                buffer.write('%');
+                char hex1 = Character.toUpperCase(Character.forDigit(
+                    (b >> 4) & 0xF, 16));
+                char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF,
+                    16));
+                buffer.write(hex1);
+                buffer.write(hex2);
+            }
+        }
+        return buffer.toByteArray();
+    }
+
+    /**
+     * Decodes an array of URL safe 7-bit characters into an array of original
+     * bytes. Escaped characters are converted back to their original
+     * representation.
+     *
+     * @param bytes array of URL safe characters
+     * @return array of original bytes
+     * @throws URIException Thrown if URL decoding is unsuccessful
+     */
+    private static final byte[] decodeUrl(byte[] bytes) {
+        if (bytes == null) {
+            return null;
+        }
+        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+        for (int i = 0; i < bytes.length; i++) {
+            int b = bytes[i];
+            if (b == '+') {
+                buffer.write(' ');
+            } else if (b == '%') {
+                try {
+                    int u = Character.digit((char) bytes[++i], 16);
+                    int l = Character.digit((char) bytes[++i], 16);
+                    if (u == -1 || l == -1) {
+                        throw new URIException("Invalid URL encoding");
+                    }
+                    buffer.write((char) ((u << 4) + l));
+                } catch (ArrayIndexOutOfBoundsException e) {
+                    throw new URIException("Invalid URL encoding");
+                }
+            } else {
+                buffer.write(b);
+            }
+        }
+        return buffer.toByteArray();
+    }
+}
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/helper/URIException.java b/src/main/java/org/apache/sling/resourceresolver/impl/helper/URIException.java
new file mode 100644
index 0000000..90a15fe
--- /dev/null
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/helper/URIException.java
@@ -0,0 +1,124 @@
+/*
+ * 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.resource.internal.helper;
+
+import org.apache.sling.api.SlingException;
+
+/**
+ * The URI parsing and escape encoding exception.
+ * <p>
+ * This class is a slightly modified version of the URIException class
+ * distributed with Http Client 3.1. The changes are removal of deprecated
+ * methods and have the class itself extend the <code>SlingException</code> to
+ * adapt it to the exception hierarchy of Sling.
+ */
+@SuppressWarnings("serial")
+public class URIException extends SlingException {
+
+    // ----------------------------------------------------------- constructors
+
+    /**
+     * Default constructor.
+     */
+    public URIException() {
+    }
+
+    /**
+     * The constructor with a reason code argument.
+     *
+     * @param reasonCode the reason code
+     */
+    public URIException(int reasonCode) {
+        this.reasonCode = reasonCode;
+    }
+
+    /**
+     * The constructor with a reason string and its code arguments.
+     *
+     * @param reasonCode the reason code
+     * @param reason the reason
+     */
+    public URIException(int reasonCode, String reason) {
+        super(reason); // for backward compatibility of Throwable
+        this.reason = reason;
+        this.reasonCode = reasonCode;
+    }
+
+    /**
+     * The constructor with a reason string argument.
+     *
+     * @param reason the reason
+     */
+    public URIException(String reason) {
+        super(reason); // for backward compatibility of Throwable
+        this.reason = reason;
+        this.reasonCode = UNKNOWN;
+    }
+
+    // -------------------------------------------------------------- constants
+
+    /**
+     * No specified reason code.
+     */
+    public static final int UNKNOWN = 0;
+
+    /**
+     * The URI parsing error.
+     */
+    public static final int PARSING = 1;
+
+    /**
+     * The unsupported character encoding.
+     */
+    public static final int UNSUPPORTED_ENCODING = 2;
+
+    /**
+     * The URI escape encoding and decoding error.
+     */
+    public static final int ESCAPING = 3;
+
+    /**
+     * The DNS punycode encoding or decoding error.
+     */
+    public static final int PUNYCODE = 4;
+
+    // ------------------------------------------------------------- properties
+
+    /**
+     * The reason code.
+     */
+    protected int reasonCode;
+
+    /**
+     * The reason message.
+     */
+    protected String reason;
+
+    // ---------------------------------------------------------------- methods
+
+    /**
+     * Get the reason code.
+     *
+     * @return the reason code
+     */
+    public int getReasonCode() {
+        return reasonCode;
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntries.java b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntries.java
new file mode 100644
index 0000000..47c0150
--- /dev/null
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntries.java
@@ -0,0 +1,782 @@
+/*
+ * 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.resource.internal.helper;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.NoSuchElementException;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.ReentrantLock;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.sling.api.SlingConstants;
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.jcr.resource.internal.JcrResourceResolver;
+import org.apache.sling.jcr.resource.internal.JcrResourceResolverFactoryImpl;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventAdmin;
+import org.osgi.service.event.EventConstants;
+import org.osgi.service.event.EventHandler;
+import org.osgi.util.tracker.ServiceTracker;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class MapEntries implements EventHandler {
+
+    public static final MapEntries EMPTY = new MapEntries();
+
+    /** Key for the global list. */
+    private static final String GLOBAL_LIST_KEY = "*";
+
+    public static final String DEFAULT_MAP_ROOT = "/etc/map";
+
+    private static final String JCR_SYSTEM_PREFIX = "/jcr:system/";
+
+    static final String ANY_SCHEME_HOST = "[^/]+/[^/]+";
+
+    /** default log */
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    private JcrResourceResolverFactoryImpl factory;
+
+    private volatile ResourceResolver resolver;
+
+    private final String mapRoot;
+
+    private Map<String, List<MapEntry>> resolveMapsMap;
+
+    private Collection<MapEntry> mapMaps;
+
+    private Collection<String> vanityTargets;
+
+    private ServiceRegistration registration;
+
+    private ServiceTracker eventAdminTracker;
+
+    private final Semaphore initTrigger = new Semaphore(0);
+
+    private final ReentrantLock initializing = new ReentrantLock();
+
+    @SuppressWarnings("unchecked")
+    private MapEntries() {
+        this.factory = null;
+        this.resolver = null;
+        this.mapRoot = DEFAULT_MAP_ROOT;
+
+        this.resolveMapsMap = Collections.singletonMap(GLOBAL_LIST_KEY, (List<MapEntry>)Collections.EMPTY_LIST);
+        this.mapMaps = Collections.<MapEntry> emptyList();
+        this.vanityTargets = Collections.<String> emptySet();
+        this.registration = null;
+        this.eventAdminTracker = null;
+    }
+
+    @SuppressWarnings("unchecked")
+    public MapEntries(final JcrResourceResolverFactoryImpl factory,
+                      final BundleContext bundleContext,
+                      final ServiceTracker eventAdminTracker)
+    throws LoginException {
+        this.resolver = factory.getAdministrativeResourceResolver(null);
+        this.factory = factory;
+        this.mapRoot = factory.getMapRoot();
+        this.eventAdminTracker = eventAdminTracker;
+
+        this.resolveMapsMap = Collections.singletonMap(GLOBAL_LIST_KEY, (List<MapEntry>)Collections.EMPTY_LIST);
+        this.mapMaps = Collections.<MapEntry> emptyList();
+        this.vanityTargets = Collections.<String> emptySet();
+
+        doInit();
+
+        final Dictionary<String, String> props = new Hashtable<String, String>();
+        props.put(EventConstants.EVENT_TOPIC, "org/apache/sling/api/resource/*");
+        props.put(EventConstants.EVENT_FILTER, createFilter());
+        props.put(Constants.SERVICE_DESCRIPTION, "Map Entries Observation");
+        props.put(Constants.SERVICE_VENDOR, "The Apache Software Foundation");
+        this.registration = bundleContext.registerService(EventHandler.class.getName(), this, props);
+
+        final Thread updateThread = new Thread(new Runnable() {
+            public void run() {
+                MapEntries.this.init();
+            }
+        }, "MapEntries Update");
+        updateThread.setDaemon(true);
+        updateThread.start();
+    }
+
+    /**
+     * Signals the init method that a the doInit method should be
+     * called.
+     */
+    private void triggerInit() {
+        // only release if there is not one in the queue already
+        if (initTrigger.availablePermits() < 1) {
+            initTrigger.release();
+        }
+    }
+
+    /**
+     * Runs as the method of the update thread. Waits for the triggerInit
+     * method to trigger a call to doInit. Terminates when the resolver
+     * has been null-ed after having been triggered.
+     */
+    void init() {
+        while (this.resolver != null) {
+            try {
+                this.initTrigger.acquire();
+                this.doInit();
+            } catch (final InterruptedException ie) {
+                // just continue acquisition
+            }
+        }
+
+    }
+
+    /**
+     * Actual initializer. Guards itself agains concurrent use by
+     * using a ReentrantLock. Does nothing if the resource resolver
+     * has already been null-ed.
+     */
+    private void doInit() {
+
+        this.initializing.lock();
+        try {
+            final ResourceResolver resolver = this.resolver;
+            final JcrResourceResolverFactoryImpl factory = this.factory;
+            if (resolver == null || factory == null) {
+                return;
+            }
+
+            final Map<String, List<MapEntry>> newResolveMapsMap = new HashMap<String, List<MapEntry>>();
+            final List<MapEntry> globalResolveMap = new ArrayList<MapEntry>();
+            final SortedMap<String, MapEntry> newMapMaps = new TreeMap<String, MapEntry>();
+
+            // load the /etc/map entries into the maps
+            loadResolverMap(resolver, globalResolveMap, newMapMaps);
+
+            // load the configuration into the resolver map
+            final Collection<String> vanityTargets = this.loadVanityPaths(resolver, newResolveMapsMap);
+            loadConfiguration(factory, globalResolveMap);
+
+            // load the configuration into the mapper map
+            loadMapConfiguration(factory, newMapMaps);
+
+            // sort global list and add to map
+            Collections.sort(globalResolveMap);
+            newResolveMapsMap.put(GLOBAL_LIST_KEY, globalResolveMap);
+
+            this.vanityTargets = Collections.unmodifiableCollection(vanityTargets);
+            this.resolveMapsMap = Collections.unmodifiableMap(newResolveMapsMap);
+            this.mapMaps = Collections.unmodifiableSet(new TreeSet<MapEntry>(newMapMaps.values()));
+
+            sendChangeEvent();
+
+        } catch (final Exception e) {
+
+            log.warn("doInit: Unexpected problem during initialization", e);
+
+        } finally {
+
+            this.initializing.unlock();
+
+        }
+    }
+
+    /**
+     * Cleans up this class.
+     */
+    public void dispose() {
+        if ( this.registration != null ) {
+            this.registration.unregister();
+            this.registration = null;
+        }
+
+        /*
+         * Cooperation with doInit: The same lock as used by doInit
+         * is acquired thus preventing doInit from running and waiting
+         * for a concurrent doInit to terminate.
+         * Once the lock has been acquired, the resource resolver is
+         * null-ed (thus causing the init to terminate when triggered
+         * the right after and prevent the doInit method from doing any
+         * thing).
+         */
+
+        // wait at most 10 seconds for a notifcation during initialization
+        boolean initLocked;
+        try {
+            initLocked = this.initializing.tryLock(10, TimeUnit.SECONDS);
+        } catch (final InterruptedException ie) {
+            initLocked = false;
+        }
+
+        try {
+            if (!initLocked) {
+                log.warn("dispose: Could not acquire initialization lock within 10 seconds; ongoing intialization may fail");
+            }
+
+            // immediately set the resolver field to null to indicate
+            // that we have been disposed (this also signals to the
+            // event handler to stop working
+            final ResourceResolver oldResolver = this.resolver;
+            this.resolver = null;
+
+            // trigger initialization to terminate init thread
+            triggerInit();
+
+            if (oldResolver != null) {
+                oldResolver.close();
+            } else {
+                log.warn("dispose: ResourceResolver has already been cleared before; duplicate call to dispose ?");
+            }
+        } finally {
+            if (initLocked) {
+                this.initializing.unlock();
+            }
+        }
+
+        // clear the rest of the fields
+        this.factory = null;
+        this.eventAdminTracker = null;
+    }
+
+    /**
+     * This is for the web console plugin
+     */
+    public List<MapEntry> getResolveMaps() {
+        final List<MapEntry> entries = new ArrayList<MapEntry>();
+        for(final List<MapEntry> list : this.resolveMapsMap.values()) {
+            entries.addAll(list);
+        }
+        Collections.sort(entries);
+        return entries;
+    }
+
+    /**
+     * Calculate the resolve maps.
+     * As the entries have to be sorted by pattern length,
+     * we have to create a new list containing all
+     * relevant entries.
+     */
+    public Iterator<MapEntry> getResolveMapsIterator(final String requestPath) {
+        String key = null;
+        final int firstIndex = requestPath.indexOf('/');
+        final int secondIndex = requestPath.indexOf('/', firstIndex + 1);
+        if ( secondIndex != -1 ) {
+            key = requestPath.substring(secondIndex);
+        }
+
+        return new MapEntryIterator(key, resolveMapsMap);
+    }
+
+    public Collection<MapEntry> getMapMaps() {
+        return mapMaps;
+    }
+
+    // ---------- EventListener interface
+
+    /**
+     * Handles the change to any of the node properties relevant for vanity URL
+     * mappings. The
+     * {@link #MapEntries(JcrResourceResolverFactoryImpl, BundleContext, ServiceTracker)}
+     * constructor makes sure the event listener is registered to only get
+     * appropriate events.
+     */
+    public void handleEvent(final Event event) {
+
+        // check for path (used for some tests below
+        final Object p = event.getProperty(SlingConstants.PROPERTY_PATH);
+        final String path;
+        if (p instanceof String) {
+            path = (String) p;
+        } else {
+            // not a string path or null, ignore this event
+            return;
+        }
+
+        // don't care for system area
+        if (path.startsWith(JCR_SYSTEM_PREFIX)) {
+            return;
+        }
+
+        // check whether a remove event has an influence on vanity paths
+        boolean doInit = true;
+        if (SlingConstants.TOPIC_RESOURCE_REMOVED.equals(event.getTopic()) && !path.startsWith(this.mapRoot)) {
+            doInit = false;
+            for (String target : this.vanityTargets) {
+                if (target.startsWith(path)) {
+                    doInit = true;
+                    break;
+                }
+            }
+        }
+
+        // trigger an update
+        if (doInit) {
+            triggerInit();
+        }
+    }
+
+    // ---------- internal
+
+    /**
+     * Send an OSGi event
+     */
+    private void sendChangeEvent() {
+        final EventAdmin ea = (EventAdmin) this.eventAdminTracker.getService();
+        if (ea != null) {
+            // we hard code the topic here and don't use
+            // SlingConstants.TOPIC_RESOURCE_RESOLVER_MAPPING_CHANGED
+            // to avoid requiring the latest API version for this bundle to work
+            final Event event = new Event("org/apache/sling/api/resource/ResourceResolverMapping/CHANGED",
+                (Dictionary<?, ?>) null);
+            ea.postEvent(event);
+        }
+    }
+
+    private void loadResolverMap(final ResourceResolver resolver,
+            List<MapEntry> entries,
+            Map<String, MapEntry> mapEntries) {
+        // the standard map configuration
+        Resource res = resolver.getResource(mapRoot);
+        if (res != null) {
+            gather(resolver, entries, mapEntries, res, "");
+        }
+    }
+
+    private void gather(final ResourceResolver resolver,
+            List<MapEntry> entries,
+            Map<String, MapEntry> mapEntries, Resource parent, String parentPath) {
+        // scheme list
+        Iterator<Resource> children = ResourceUtil.listChildren(parent);
+        while (children.hasNext()) {
+            final Resource child = children.next();
+            final ValueMap vm = ResourceUtil.getValueMap(child);
+
+            String name = vm.get(JcrResourceResolver.PROP_REG_EXP, String.class);
+            boolean trailingSlash = false;
+            if (name == null) {
+                name = ResourceUtil.getName(child).concat("/");
+                trailingSlash = true;
+            }
+
+            String childPath = parentPath.concat(name);
+
+            // gather the children of this entry (only if child is not end hooked)
+            if (!childPath.endsWith("$")) {
+
+                // add trailing slash to child path to append the child
+                String childParent = childPath;
+                if (!trailingSlash) {
+                    childParent = childParent.concat("/");
+                }
+
+                gather(resolver, entries, mapEntries, child, childParent);
+            }
+
+            // add resolution entries for this node
+            final MapEntry childResolveEntry = MapEntry.createResolveEntry(childPath,
+                child, trailingSlash);
+            if (childResolveEntry != null) {
+                entries.add(childResolveEntry);
+            }
+
+            // add map entries for this node
+            List<MapEntry> childMapEntries = MapEntry.createMapEntry(childPath,
+                child, trailingSlash);
+            if (childMapEntries != null) {
+                for (MapEntry mapEntry : childMapEntries) {
+                    addMapEntry(mapEntries, mapEntry.getPattern(),
+                        mapEntry.getRedirect()[0], mapEntry.getStatus());
+                }
+            }
+
+        }
+    }
+
+    /**
+     * Add an entry to the resolve map.
+     */
+    private void addEntry(final Map<String, List<MapEntry>> entryMap,
+            final String key, final MapEntry entry) {
+        List<MapEntry> entries = entryMap.get(key);
+        if ( entries == null ) {
+            entries = new ArrayList<MapEntry>();
+            entryMap.put(key, entries);
+        }
+        entries.add(entry);
+        // and finally sort list
+        Collections.sort(entries);
+    }
+
+    /**
+     * Load vanity paths
+     * Search for all nodes inheriting the sling:VanityPath mixin
+     */
+    private Collection<String> loadVanityPaths(final ResourceResolver resolver,
+            final Map<String, List<MapEntry>> entryMap) {
+        // sling:VanityPath (uppercase V) is the mixin name
+        // sling:vanityPath (lowercase) is the property name
+        final Set<String> targetPaths = new HashSet<String>();
+        final String queryString = "SELECT sling:vanityPath, sling:redirect, sling:redirectStatus FROM sling:VanityPath WHERE sling:vanityPath IS NOT NULL ORDER BY sling:vanityOrder DESC";
+        final Iterator<Resource> i = resolver.findResources(queryString, "sql");
+
+        while (i.hasNext()) {
+            final Resource resource = i.next();
+
+            // ignore system tree
+            if (resource.getPath().startsWith(JCR_SYSTEM_PREFIX)) {
+                log.debug("loadVanityPaths: Ignoring {}", resource);
+                continue;
+            }
+
+            // require properties
+            final ValueMap props = resource.adaptTo(ValueMap.class);
+            if (props == null) {
+                log.debug("loadVanityPaths: Ignoring {} without properties", resource);
+                continue;
+            }
+
+            // url is ignoring scheme and host.port and the path is
+            // what is stored in the sling:vanityPath property
+            final String[] pVanityPaths = props.get("sling:vanityPath", new String[0]);
+            for (final String pVanityPath : pVanityPaths) {
+                final String[] result = this.getVanityPathDefinition(pVanityPath);
+                if ( result != null ) {
+                    final String url = result[0] + result[1];
+
+                    // redirect target is the node providing the sling:vanityPath
+                    // property (or its parent if the node is called jcr:content)
+                    final String redirect;
+                    if (resource.getName().equals("jcr:content")) {
+                        redirect = resource.getParent().getPath();
+                    } else {
+                        redirect = resource.getPath();
+                    }
+
+                    // whether the target is attained by a 302/FOUND or by an
+                    // internal redirect is defined by the sling:redirect property
+                    final int status = props.get("sling:redirect", false)
+                            ? props.get(JcrResourceResolver.PROP_REDIRECT_EXTERNAL_REDIRECT_STATUS, HttpServletResponse.SC_FOUND)
+                            : -1;
+
+                    final String checkPath = result[1];
+                    // 1. entry with exact match
+                    this.addEntry(entryMap, checkPath, new MapEntry(url + "$", status, false, redirect
+                            + ".html"));
+
+                    // 2. entry with match supporting selectors and extension
+                    this.addEntry(entryMap, checkPath, new MapEntry(url + "(\\..*)", status, false,
+                            redirect + "$1"));
+
+                    // 3. keep the path to return
+                    targetPaths.add(redirect);
+                }
+            }
+        }
+        return targetPaths;
+    }
+
+    /**
+     * Create the vanity path definition. String array containing:
+     * {protocol}/{host}[.port]
+     * {absolute path}
+     */
+    private String[] getVanityPathDefinition(final String pVanityPath) {
+        String[] result = null;
+        if ( pVanityPath != null ) {
+            final String info = pVanityPath.trim();
+            if ( info.length() > 0 ) {
+                String prefix = null;
+                String path = null;
+                // check for url
+                if ( info.indexOf(":/") > - 1 ) {
+                    try {
+                        final URL u = new URL(info);
+                        prefix = u.getProtocol() + '/' + u.getHost() + '.' + u.getPort();
+                        path = u.getPath();
+                    } catch (final MalformedURLException e) {
+                        log.warn("Ignoring malformed vanity path {}", pVanityPath);
+                    }
+                } else {
+                    prefix = "^" + ANY_SCHEME_HOST;
+                    if ( !info.startsWith("/") ) {
+                        path = "/" + info;
+                    } else {
+                        path = info;
+                    }
+                }
+
+                // remove extension
+                if ( prefix != null ) {
+                    final int lastSlash = path.lastIndexOf('/');
+                    final int firstDot = path.indexOf('.', lastSlash + 1);
+                    if ( firstDot != -1 ) {
+                        path = path.substring(0, firstDot);
+                        log.warn("Removing extension from vanity path {}", pVanityPath);
+                    }
+                    result = new String[] {prefix, path};
+                }
+            }
+        }
+        return result;
+    }
+
+    private void loadConfiguration(final JcrResourceResolverFactoryImpl factory,
+            final List<MapEntry> entries) {
+        // virtual uris
+        final Map<?, ?> virtuals = factory.getVirtualURLMap();
+        if (virtuals != null) {
+            for (final Entry<?, ?> virtualEntry : virtuals.entrySet()) {
+                final String extPath = (String) virtualEntry.getKey();
+                final String intPath = (String) virtualEntry.getValue();
+                if (!extPath.equals(intPath)) {
+                    // this regular expression must match the whole URL !!
+                    final String url = "^" + ANY_SCHEME_HOST + extPath + "$";
+                    final String redirect = intPath;
+                    entries.add(new MapEntry(url, -1, false, redirect));
+                }
+            }
+        }
+
+        // URL Mappings
+        final Mapping[] mappings = factory.getMappings();
+        if (mappings != null) {
+            Map<String, List<String>> map = new HashMap<String, List<String>>();
+            for (Mapping mapping : mappings) {
+                if (mapping.mapsInbound()) {
+                    String url = mapping.getTo();
+                    String alias = mapping.getFrom();
+                    if (url.length() > 0) {
+                        List<String> aliasList = map.get(url);
+                        if (aliasList == null) {
+                            aliasList = new ArrayList<String>();
+                            map.put(url, aliasList);
+                        }
+                        aliasList.add(alias);
+                    }
+                }
+            }
+
+            for (final Entry<String, List<String>> entry : map.entrySet()) {
+                entries.add(new MapEntry(ANY_SCHEME_HOST + entry.getKey(),
+                        -1, false, entry.getValue().toArray(new String[0])));
+            }
+        }
+    }
+
+    private void loadMapConfiguration(JcrResourceResolverFactoryImpl factory,
+            Map<String, MapEntry> entries) {
+        // URL Mappings
+        Mapping[] mappings = factory.getMappings();
+        if (mappings != null) {
+            for (int i = mappings.length - 1; i >= 0; i--) {
+                Mapping mapping = mappings[i];
+                if (mapping.mapsOutbound()) {
+                    String url = mapping.getTo();
+                    String alias = mapping.getFrom();
+                    if (!url.equals(alias)) {
+                        addMapEntry(entries, alias, url, -1);
+                    }
+                }
+            }
+        }
+
+        // virtual uris
+        Map<?, ?> virtuals = factory.getVirtualURLMap();
+        if (virtuals != null) {
+            for (Entry<?, ?> virtualEntry : virtuals.entrySet()) {
+                String extPath = (String) virtualEntry.getKey();
+                String intPath = (String) virtualEntry.getValue();
+                if (!extPath.equals(intPath)) {
+                    // this regular expression must match the whole URL !!
+                    String path = "^" + intPath + "$";
+                    String url = extPath;
+                    addMapEntry(entries, path, url, -1);
+                }
+            }
+        }
+    }
+
+    private void addMapEntry(Map<String, MapEntry> entries, String path,
+            String url, int status) {
+        MapEntry entry = entries.get(path);
+        if (entry == null) {
+            entry = new MapEntry(path, status, false, url);
+        } else {
+            String[] redir = entry.getRedirect();
+            String[] newRedir = new String[redir.length + 1];
+            System.arraycopy(redir, 0, newRedir, 0, redir.length);
+            newRedir[redir.length] = url;
+            entry = new MapEntry(entry.getPattern(), entry.getStatus(),
+                false, newRedir);
+        }
+        entries.put(path, entry);
+    }
+
+    /**
+     * Returns a filter which matches if any of the nodeProps (JCR properties
+     * modified) is listed in any of the eventProps (event properties listing
+     * modified JCR properties) this allows to only get events interesting for
+     * updating the internal structure
+     */
+    private static String createFilter() {
+        final String[] nodeProps = {
+            "sling:vanityPath", "sling:vanityOrder", JcrResourceResolver.PROP_REDIRECT_EXTERNAL_REDIRECT_STATUS,
+            JcrResourceResolver.PROP_REDIRECT_EXTERNAL, JcrResourceResolver.PROP_REDIRECT_INTERNAL,
+            JcrResourceResolver.PROP_REDIRECT_EXTERNAL_STATUS, JcrResourceResolver.PROP_REG_EXP
+        };
+        final String[] eventProps = {
+            "resourceAddedAttributes", "resourceChangedAttributes", "resourceRemovedAttributes"
+        };
+        StringBuilder filter = new StringBuilder();
+        filter.append("(|");
+        for (String eventProp : eventProps) {
+            filter.append("(|");
+            for (String nodeProp : nodeProps) {
+                filter.append('(').append(eventProp).append('=').append(nodeProp).append(')');
+            }
+            filter.append(")");
+        }
+        filter.append("(" + EventConstants.EVENT_TOPIC + "=" + SlingConstants.TOPIC_RESOURCE_REMOVED + ")");
+        filter.append(")");
+
+        return filter.toString();
+    }
+
+    private static final class MapEntryIterator implements Iterator<MapEntry> {
+
+        private final Map<String, List<MapEntry>> resolveMapsMap;
+
+        private String key;
+
+        private MapEntry next;
+
+        private Iterator<MapEntry> globalListIterator;
+        private MapEntry nextGlobal;
+
+        private Iterator<MapEntry> specialIterator;
+        private MapEntry nextSpecial;
+
+        public MapEntryIterator(final String startKey, final Map<String, List<MapEntry>> resolveMapsMap) {
+            this.key = startKey;
+            this.resolveMapsMap = resolveMapsMap;
+            this.globalListIterator = this.resolveMapsMap.get(GLOBAL_LIST_KEY).iterator();
+            this.seek();
+        }
+
+
+        /**
+         * @see java.util.Iterator#hasNext()
+         */
+        public boolean hasNext() {
+            return this.next != null;
+        }
+
+        /**
+         * @see java.util.Iterator#next()
+         */
+        public MapEntry next() {
+            if ( this.next == null ) {
+                throw new NoSuchElementException();
+            }
+            final MapEntry result = this.next;
+            this.seek();
+            return result;
+        }
+
+        /**
+         * @see java.util.Iterator#remove()
+         */
+        public void remove() {
+            throw new UnsupportedOperationException();
+        }
+
+        private void seek() {
+            if ( this.nextGlobal == null && this.globalListIterator.hasNext() ) {
+                this.nextGlobal = this.globalListIterator.next();
+            }
+            if ( this.nextSpecial == null ) {
+                if ( specialIterator != null && !specialIterator.hasNext() ) {
+                    specialIterator = null;
+                }
+                while ( specialIterator == null && key != null ) {
+                    // remove selectors and extension
+                    final int lastSlashPos = key.lastIndexOf('/');
+                    final int lastDotPos = key.indexOf('.', lastSlashPos);
+                    if ( lastDotPos != -1 ) {
+                        key = key.substring(0, lastDotPos);
+                    }
+                    final List<MapEntry> special = this.resolveMapsMap.get(key);
+                    if ( special != null ) {
+                        specialIterator = special.iterator();
+                    }
+                    // recurse to the parent
+                    if ( key.length() > 1 ) {
+                        final int lastSlash = key.lastIndexOf("/");
+                        if ( lastSlash == 0 ) {
+                            key = null;
+                        } else {
+                            key = key.substring(0, lastSlash);
+                        }
+                    } else {
+                        key = null;
+                    }
+                }
+                if ( this.specialIterator != null && this.specialIterator.hasNext() ) {
+                    this.nextSpecial = this.specialIterator.next();
+                }
+            }
+            if ( this.nextSpecial == null ) {
+                this.next = this.nextGlobal;
+                this.nextGlobal = null;
+            } else if ( this.nextGlobal == null ) {
+                this.next = this.nextSpecial;
+                this.nextSpecial = null;
+            } else if ( this.nextGlobal.getPattern().length() >= this.nextSpecial.getPattern().length() ) {
+                this.next = this.nextGlobal;
+                this.nextGlobal = null;
+            } else {
+                this.next = this.nextSpecial;
+                this.nextSpecial = null;
+            }
+        }
+    };
+}
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntry.java b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntry.java
new file mode 100644
index 0000000..be94165
--- /dev/null
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntry.java
@@ -0,0 +1,331 @@
+/*
+ * 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.resource.internal.helper;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.jcr.resource.internal.JcrResourceResolver;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The <code>MapEntry</code> class represents a mapping entry in the mapping
+ * configuration tree at <code>/etc/map</code>.
+ * <p>
+ *
+ * @see "http://cwiki.apache.org/SLING/flexible-resource-resolution.html"
+ */
+public class MapEntry implements Comparable<MapEntry> {
+
+    private static final Pattern[] URL_WITH_PORT_MATCH = {
+        Pattern.compile("http/([^/]+)(\\.[^\\d/]+)(/.*)?$"),
+        Pattern.compile("https/([^/]+)(\\.[^\\d/]+)(/.*)?$") };
+
+    private static final String[] URL_WITH_PORT_REPLACEMENT = {
+        "http/$1$2.80$3", "https/$1$2.443$3" };
+
+    private static final Pattern[] PATH_TO_URL_MATCH = {
+        Pattern.compile("http/([^/]+)\\.80(/.*)?$"),
+        Pattern.compile("https/([^/]+)\\.443(/.*)?$"),
+        Pattern.compile("([^/]+)/([^/]+)\\.(\\d+)(/.*)?$"),
+        Pattern.compile("([^/]+)/([^/]+)(/.*)?$") };
+
+    private static final String[] PATH_TO_URL_REPLACEMENT = { "http://$1$2",
+        "https://$1$2", "$1://$2:$3$4", "$1://$2$3" };
+
+    private final Pattern urlPattern;
+
+    private final String[] redirect;
+
+    private final int status;
+
+    public static String appendSlash(String path) {
+        if (!path.endsWith("/")) {
+            path = path.concat("/");
+        }
+        return path;
+    }
+
+    /**
+     * Returns a string used for matching map entries against the given request
+     * or URI parts.
+     *
+     * @param scheme The URI scheme
+     * @param host The host name
+     * @param port The port number. If this is negative, the default value used
+     *            is 80 unless the scheme is "https" in which case the default
+     *            value is 443.
+     * @param path The (absolute) path
+     * @return The request path string {scheme}://{host}:{port}{path}.
+     */
+    public static String getURI(String scheme, String host, int port,
+            String path) {
+
+        StringBuilder sb = new StringBuilder();
+        sb.append(scheme).append("://").append(host);
+        if (port > 0 && !(port == 80 && "http".equals(scheme))
+            && !(port == 443 && "https".equals(scheme))) {
+            sb.append(':').append(port);
+        }
+        sb.append(path);
+
+        return sb.toString();
+    }
+
+    public static String fixUriPath(String uriPath) {
+        for (int i = 0; i < URL_WITH_PORT_MATCH.length; i++) {
+            Matcher m = URL_WITH_PORT_MATCH[i].matcher(uriPath);
+            if (m.find()) {
+                return m.replaceAll(URL_WITH_PORT_REPLACEMENT[i]);
+            }
+        }
+
+        return uriPath;
+    }
+
+    public static URI toURI(String uriPath) {
+        for (int i = 0; i < PATH_TO_URL_MATCH.length; i++) {
+            Matcher m = PATH_TO_URL_MATCH[i].matcher(uriPath);
+            if (m.find()) {
+                String newUriPath = m.replaceAll(PATH_TO_URL_REPLACEMENT[i]);
+                try {
+                    return new URI(newUriPath);
+                } catch (URISyntaxException use) {
+                    // ignore, just don't return the uri as such
+                }
+            }
+        }
+
+        return null;
+    }
+
+    public static MapEntry createResolveEntry(String url, Resource resource,
+            boolean trailingSlash) {
+        ValueMap props = resource.adaptTo(ValueMap.class);
+        if (props != null) {
+
+            // ensure the url contains a port number (if possible)
+            url = fixUriPath(url);
+
+            String redirect = props.get(
+                JcrResourceResolver.PROP_REDIRECT_EXTERNAL, String.class);
+            if (redirect != null) {
+                int status = props.get(
+                    JcrResourceResolver.PROP_REDIRECT_EXTERNAL_STATUS, 302);
+                return new MapEntry(url, status, trailingSlash, redirect);
+            }
+
+            String[] internalRedirect = props.get(
+                JcrResourceResolver.PROP_REDIRECT_INTERNAL, String[].class);
+            if (internalRedirect != null) {
+                return new MapEntry(url, -1, trailingSlash, internalRedirect);
+            }
+        }
+
+        return null;
+    }
+
+    public static List<MapEntry> createMapEntry(String url, Resource resource,
+            boolean trailingSlash) {
+        ValueMap props = resource.adaptTo(ValueMap.class);
+        if (props != null) {
+            String redirect = props.get(
+                JcrResourceResolver.PROP_REDIRECT_EXTERNAL, String.class);
+            if (redirect != null) {
+                // ignoring external redirects for mapping
+                LoggerFactory.getLogger(MapEntry.class).info(
+                    "createMapEntry: Configuration has external redirect to {}; not creating mapping for configuration in {}",
+                    redirect, resource.getPath());
+                return null;
+            }
+
+            // ignore potential regular expression url
+            if (isRegExp(url)) {
+                LoggerFactory.getLogger(MapEntry.class).info(
+                    "createMapEntry: URL {} contains a regular expression; not creating mapping for configuration in {}",
+                    url, resource.getPath());
+
+                return null;
+            }
+
+            // check whether the url is a match hooked to then string end
+            String endHook = "";
+            if (url.endsWith("$")) {
+                endHook = "$";
+                url = url.substring(0, url.length()-1);
+            }
+
+            // check whether the url is for ANY_SCHEME_HOST
+            if (url.startsWith(MapEntries.ANY_SCHEME_HOST)) {
+                url = url.substring(MapEntries.ANY_SCHEME_HOST.length());
+            }
+
+            String[] internalRedirect = props.get(
+                JcrResourceResolver.PROP_REDIRECT_INTERNAL, String[].class);
+            if (internalRedirect != null) {
+
+                int status = -1;
+                URI extPathPrefix = toURI(url);
+                if (extPathPrefix != null) {
+                    url = getURI(extPathPrefix.getScheme(),
+                        extPathPrefix.getHost(), extPathPrefix.getPort(),
+                        extPathPrefix.getPath());
+                    status = 302;
+                }
+
+                List<MapEntry> prepEntries = new ArrayList<MapEntry>(
+                    internalRedirect.length);
+                for (String redir : internalRedirect) {
+                    if (!redir.contains("$")) {
+                        prepEntries.add(new MapEntry(redir.concat(endHook),
+                            status, trailingSlash, url));
+                    }
+                }
+
+                if (prepEntries.size() > 0) {
+                    return prepEntries;
+                }
+            }
+        }
+
+        return null;
+    }
+
+    public MapEntry(String url, int status, boolean trailingSlash,
+            String... redirect) {
+
+        // ensure trailing slashes on redirects if the url
+        // ends with a trailing slash
+        if (trailingSlash) {
+            url = appendSlash(url);
+            for (int i = 0; i < redirect.length; i++) {
+                redirect[i] = appendSlash(redirect[i]);
+            }
+        }
+
+        // ensure pattern is hooked to the start of the string
+        if (!url.startsWith("^")) {
+            url = "^".concat(url);
+        }
+
+        this.urlPattern = Pattern.compile(url);
+        this.redirect = redirect;
+        this.status = status;
+    }
+
+    // Returns the replacement or null if the value does not match
+    public String[] replace(String value) {
+        Matcher m = urlPattern.matcher(value);
+        if (m.find()) {
+            String[] redirects = getRedirect();
+            String[] results = new String[redirects.length];
+            for (int i = 0; i < redirects.length; i++) {
+                results[i] = m.replaceFirst(redirects[i]);
+            }
+            return results;
+        }
+
+        return null;
+    }
+
+    public String getPattern() {
+        return urlPattern.toString();
+    }
+
+    public String[] getRedirect() {
+        return redirect;
+    }
+
+    public boolean isInternal() {
+        return getStatus() < 0;
+    }
+
+    public int getStatus() {
+        return status;
+    }
+
+    // ---------- Comparable
+
+    public int compareTo(MapEntry m) {
+        if (this == m) {
+            return 0;
+        }
+
+        int tlen = urlPattern.toString().length();
+        int mlen = m.urlPattern.toString().length();
+        if (tlen < mlen) {
+            return 1;
+        } else if (tlen > mlen) {
+            return -1;
+        }
+
+        // lentghs are equal, but the entries are not
+        // so order m after this
+        return 1;
+    }
+
+    // ---------- Object overwrite
+
+    @Override
+    public String toString() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("MapEntry: match:").append(urlPattern);
+
+        buf.append(", replacement:");
+        if (getRedirect().length == 1) {
+            buf.append(getRedirect()[0]);
+        } else {
+            buf.append(Arrays.asList(getRedirect()));
+        }
+
+        if (isInternal()) {
+            buf.append(", internal");
+        } else {
+            buf.append(", status:").append(getStatus());
+        }
+        return buf.toString();
+    }
+
+    //---------- helper
+
+    /**
+     * Returns <code>true</code> if the string contains unescaped regular
+     * expression special characters '+', '*', '?', '|', '(', '), '[', and ']'
+     * @param string
+     * @return
+     */
+    private static boolean isRegExp(final String string) {
+        for (int i=0; i < string.length(); i++) {
+            char c = string.charAt(i);
+            if (c == '\\') {
+                i++; // just skip
+            } else if ("+*?|()[]".indexOf(c) >= 0) {
+                return true; // assume an unescaped pattern character
+            }
+        }
+        return false;
+    }
+}
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/mapping/Mapping.java b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/Mapping.java
new file mode 100644
index 0000000..b6b9087
--- /dev/null
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/Mapping.java
@@ -0,0 +1,207 @@
+/*
+ * 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.resource.internal.helper;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * The <code>Mapping</code> class conveys the mapping configuration used by
+ * the
+ * {@link org.apache.sling.jcr.resource.internal.JcrResourceResolverFactoryImpl}.
+ */
+public class Mapping {
+
+    /**
+     * defines the 'inbound' direction, that is mapping request path to item
+     * path
+     */
+    public static final int INBOUND = 1;
+
+    /** defined the 'outbound' direction, that is mapping item path to URL path */
+    public static final int OUTBOUND = 2;
+
+    /** defines the 'both' direction */
+    public static final int BOTH = 3;
+
+    /** Simple mapper instance mapping path to URLs 1:1 in both directions */
+    public static final Mapping DIRECT = new Mapping("", "", BOTH) {
+
+        @Override
+        public String mapHandle(String handle) {
+            return handle;
+        }
+
+        @Override
+        public boolean mapsInbound() {
+            return true;
+        }
+
+        @Override
+        public boolean mapsOutbound() {
+            return true;
+        }
+
+        @Override
+        public String mapUri(String uriPath) {
+            return uriPath;
+        }
+    };
+
+    // Regular expression to split mapping configuration strings into three
+    // groups:
+    //   1 - external path prefix
+    //   2 - direction (Outbound (>), Bidirectional (:), Inbound (>))
+    //   3 - internap path prefix
+    private static final Pattern CONFIG_SPLITTER = Pattern.compile("(.+)([:<>])(.+)");
+
+    /** the 'from' (inside, repository) mapping */
+    private final String from;
+
+    /** the 'to' (outside, URL) mapping */
+    private final String to;
+
+    /** the length of the 'from' field */
+    private final int fromLength;
+
+    /** the length of the 'to' field */
+    private final int toLength;
+
+    /** the mapping direction */
+    private final int direction;
+
+    public Mapping(String config) {
+        this(split(config));
+    }
+
+    public Mapping(String[] parts) {
+        this.from = parts[0];
+        this.to = parts[2];
+        this.fromLength = this.from.length();
+        this.toLength = this.to.length();
+
+        this.direction = ">".equals(parts[1])
+                ? Mapping.INBOUND
+                : ("<".equals(parts[1]) ? Mapping.OUTBOUND : Mapping.BOTH);
+    }
+
+    @Override
+    public String toString() {
+        return "Mapping (from=" + from + ", to=" + to + ", direction=" + direction
+            + ", lengths=" + fromLength + "/" + toLength;
+    }
+
+    /**
+     * Replaces the prefix <em>to</em> by the new prefix <em>from</em>, if
+     * and only if <code>uriPath</code> starts with the <em>to</em> prefix.
+     * If <code>uriPath</code> does not start with the <em>to</em> prefix,
+     * or if this mapping is not defined as a 'inward' mapping,
+     * <code>null</code> is returned.
+     *
+     * @param uriPath The URI path for which to replace the <em>to</em> prefix
+     *            by the <em>from</em> prefix.
+     * @return The string after replacement or <code>null</code> if the
+     *         <code>uriPath</code> does not start with the <em>to</em>
+     *         prefix, or {@link #mapsInbound()} returns <code>false</code>.
+     */
+    public String mapUri(String uriPath) {
+        return (this.mapsInbound() && uriPath.startsWith(this.to)) ? this.from
+            + uriPath.substring(this.toLength) : null;
+    }
+
+    /**
+     * Replaces the prefix <em>from</em> by the new prefix <em>to</em>, if
+     * and only if <code>handle</code> starts with the <em>from</em> prefix.
+     * If <code>uriPath</code> does not start with the <em>from</em> prefix,
+     * or if this mapping is not defined as a 'outward' mapping,
+     * <code>null</code> is returned.
+     *
+     * @param handle The URI path for which to replace the <em>from</em>
+     *            prefix by the <em>to</em> prefix.
+     * @return The string after replacement or <code>null</code> if the
+     *         <code>handle</code> does not start with the <em>from</em>
+     *         prefix, or {@link #mapsOutbound()} returns <code>false</code>.
+     */
+    public String mapHandle(String handle) {
+        return (this.mapsOutbound() && handle.startsWith(this.from)) ? this.to
+            + handle.substring(this.fromLength) : null;
+    }
+
+    // TODO: temporary
+    public String getFrom() {
+        return from;
+    }
+
+    // TODO: temporary
+    public String getTo() {
+        return to;
+    }
+
+    /**
+     * Checks, if this mapping is defined for inbound mapping.
+     *
+     * @return <code>true</code> if this mapping is defined for inbound
+     *         mapping; <code>false</code> otherwise
+     */
+    public boolean mapsInbound() {
+        return (this.direction & Mapping.INBOUND) > 0;
+    }
+
+    /**
+     * Checks, if this mapping is defined for outbound mapping.
+     *
+     * @return <code>true</code> if this mapping is defined for outbound
+     *         mapping; <code>false</code> otherwise
+     */
+    public boolean mapsOutbound() {
+        return (this.direction & Mapping.OUTBOUND) > 0;
+    }
+
+    /**
+     * Constructs a new mapping with the given mapping string and the direction
+     */
+    private Mapping(String from, String to, int dir) {
+        this.from = from;
+        this.to = to;
+        this.fromLength = from.length();
+        this.toLength = to.length();
+        this.direction = dir;
+    }
+
+    public static String[] split(String map) {
+
+        // standard case of mapping <path>[<:>]<path>
+        Matcher mapMatch = CONFIG_SPLITTER.matcher(map);
+        if (mapMatch.matches()) {
+            return new String[] { mapMatch.group(1), mapMatch.group(2),
+                mapMatch.group(3) };
+        }
+
+        // backwards compatibility using "-" instead of ":"
+        int dash = map.indexOf('-');
+        if (dash > 0) {
+            return new String[] { map.substring(0, dash),
+                map.substring(dash, dash + 1),
+                map.substring(dash + 1, map.length()) };
+        }
+
+        return new String[] { map, "-", map };
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/tree/ProviderHandler.java b/src/main/java/org/apache/sling/resourceresolver/impl/tree/ProviderHandler.java
new file mode 100644
index 0000000..1700b77
--- /dev/null
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/tree/ProviderHandler.java
@@ -0,0 +1,107 @@
+/*
+ * 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 SF 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.resource.internal.helper;
+
+import java.util.Iterator;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceProvider;
+import org.apache.sling.api.resource.ResourceResolver;
+
+/**
+ *
+ */
+public class WrappedResourceProvider  implements ResourceProvider {
+
+    private ResourceProvider resourceProvider;
+    private Comparable<?> serviceReference;
+
+    /**
+     *
+     */
+    public WrappedResourceProvider(ResourceProvider resourceProvider, Comparable<?> serviceReference) {
+        this.resourceProvider = resourceProvider;
+        this.serviceReference = serviceReference;
+    }
+
+    /**
+     * {@inheritDoc}
+     * @see org.apache.sling.api.resource.ResourceProvider#getResource(org.apache.sling.api.resource.ResourceResolver, java.lang.String)
+     */
+    public Resource getResource(ResourceResolver arg0, String arg1) {
+        return resourceProvider.getResource(arg0, arg1);
+    }
+
+    /**
+     * {@inheritDoc}
+     * @see org.apache.sling.api.resource.ResourceProvider#getResource(org.apache.sling.api.resource.ResourceResolver, javax.servlet.http.HttpServletRequest, java.lang.String)
+     */
+    public Resource getResource(ResourceResolver arg0, HttpServletRequest arg1, String arg2) {
+        return resourceProvider.getResource(arg0, arg1, arg2);
+    }
+
+    /**
+     * {@inheritDoc}
+     * @see org.apache.sling.api.resource.ResourceProvider#listChildren(org.apache.sling.api.resource.Resource)
+     */
+    public Iterator<Resource> listChildren(Resource arg0) {
+        return resourceProvider.listChildren(arg0);
+    }
+
+    /**
+     *
+     */
+    public Comparable<?> getComparable() {
+        return serviceReference;
+    }
+
+    /**
+     * {@inheritDoc}
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        return resourceProvider.hashCode();
+    }
+
+    /**
+     * {@inheritDoc}
+     * @see java.lang.Object#equals(java.lang.Object)
+     */
+    @Override
+    public boolean equals(Object obj) {
+        if ( obj instanceof WrappedResourceProvider ) {
+            return resourceProvider.equals(((WrappedResourceProvider) obj).resourceProvider);
+        } else if ( obj instanceof ResourceProvider) {
+            return resourceProvider.equals(obj);
+        }
+        return super.equals(obj);
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @see java.lang.Object#toString()
+     */
+    @Override
+    public String toString() {
+        return resourceProvider.toString();
+    }
+}
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/tree/ResourceProviderEntry.java b/src/main/java/org/apache/sling/resourceresolver/impl/tree/ResourceProviderEntry.java
new file mode 100644
index 0000000..c5c4e76
--- /dev/null
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/tree/ResourceProviderEntry.java
@@ -0,0 +1,464 @@
+/*
+ * 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.resource.internal.helper;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.commons.collections.FastTreeMap;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceProvider;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.SyntheticResource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The <code>ResourceProviderEntry</code> class represents a node in the tree of
+ * resource providers spanned by the root paths of the provider resources.
+ * <p>
+ * This class is comparable to itself to help keep the child entries list sorted
+ * by their prefix.
+ */
+public class ResourceProviderEntry implements
+        Comparable<ResourceProviderEntry> {
+
+    /**
+     *
+     */
+    private static final long serialVersionUID = 7420631325909144862L;
+
+    private static Logger LOGGER = LoggerFactory.getLogger(ResourceProviderEntry.class);
+
+    // the path to resources provided by the resource provider of this
+    // entry. this path is relative to the path of the parent resource
+    // provider entry and has no trailing slash.
+    private final String path;
+
+    // the path to resources provided by the resource provider of this
+    // entry. this is the same path as the path field but with a trailing
+    // slash to be used as a prefix match resource paths to resolve
+    private final String prefix;
+
+    // the resource provider kept in this entry supporting resources at and
+    // below the path of this entry.
+    private WrappedResourceProvider[] providers = new WrappedResourceProvider[0];
+
+    private long ttime = 0L;
+
+    private long nmiss = 0L;
+
+    private long nsynthetic = 0L;
+
+    private long nreal = 0L;
+
+    private FastTreeMap storageMap = new FastTreeMap();
+
+    private Collection<ResourceProviderEntry> storageMapValues = new ArrayList<ResourceProviderEntry>();
+
+    /**
+     * Creates an instance of this class with the given path relative to the
+     * parent resource provider entry, encapsulating the given ResourceProvider,
+     * and a number of inital child entries.
+     *
+     * @param path
+     *            The relative path supported by the provider
+     * @param providerList
+     *            The resource provider to encapsulate by this entry.
+     */
+    public ResourceProviderEntry(String path, ResourceProvider[] providerList) {
+        if (path.endsWith("/")) {
+            this.path = path.substring(0, path.length() - 1);
+            this.prefix = path;
+        } else {
+            this.path = path;
+            this.prefix = path + "/";
+        }
+        if ( providerList != null ) {
+          providers = new WrappedResourceProvider[providerList.length];
+          for ( int i = 0; i < providerList.length; i++ ) {
+            if ( providerList[i] instanceof WrappedResourceProvider ) {
+              providers[i] = (WrappedResourceProvider) providerList[i];
+            } else {
+              providers[i] = new WrappedResourceProvider(providerList[i], null);
+            }
+          }
+        }
+
+        // this will consume slightly more memory but ensures read is fast.
+        storageMap.setFast(true);
+
+    }
+
+    String getPath() {
+        return path;
+    }
+
+    /**
+     * Returns the resource provider contained in this entry
+     */
+    public ResourceProvider[] getResourceProviders() {
+        return providers;
+    }
+
+    /**
+     * Returns the resource with the given path or <code>null</code> if neither
+     * the resource provider of this entry nor the resource provider of any of
+     * the child entries can provide the resource.
+     *
+     * @param path
+     *            The path to the resource to return.
+     * @return The resource for the path or <code>null</code> if no resource can
+     *         be found.
+     * @throws org.apache.sling.api.SlingException
+     *             if an error occurrs trying to access an existing resource.
+     */
+    public Resource getResource(ResourceResolver resourceResolver, String path) {
+        return getInternalResource(resourceResolver, path);
+    }
+
+    /**
+     * Adds the given resource provider into the tree for the given prefix.
+     *
+     * @return <code>true</code> if the provider could be entered into the
+     *         subtree below this entry. Otherwise <code>false</code> is
+     *         returned.
+     */
+    public boolean addResourceProvider(String prefix, ResourceProvider provider, Comparable<?> comparable) {
+        synchronized (this) {
+            String[] elements = split(prefix, '/');
+            List<ResourceProviderEntry> entryPath = new ArrayList<ResourceProviderEntry>();
+            entryPath.add(this); // add this the start so if the list is empty we have a position to add to
+            populateProviderPath(entryPath, elements);
+            for (int i = entryPath.size() - 1; i < elements.length; i++) {
+                String stubPrefix = elements[i];
+                ResourceProviderEntry rpe2 = new ResourceProviderEntry(
+                        stubPrefix, new ResourceProvider[0]);
+                entryPath.get(i).put(elements[i], rpe2);
+                entryPath.add(rpe2);
+            }
+            return entryPath.get(elements.length).addInternalProvider(new WrappedResourceProvider(provider, comparable));
+
+        }
+    }
+
+
+    //------------------ Map methods, here so that we can delegate 2 maps together
+    @SuppressWarnings("unchecked")
+    public void put(String key, ResourceProviderEntry value) {
+        storageMap.put(key,value);
+        // get a thread safe copy, the ArrayList constructor does a toArray which is thread safe.
+        storageMapValues = new ArrayList<ResourceProviderEntry>(storageMap.values());
+    }
+
+    public boolean containsKey(String key) {
+        return storageMap.containsKey(key);
+    }
+
+    public ResourceProviderEntry get(String key) {
+        return (ResourceProviderEntry) storageMap.get(key);
+    }
+
+    public Collection<ResourceProviderEntry> values() {
+        return storageMapValues;
+    }
+
+    public boolean removeResourceProvider(String prefix,
+            ResourceProvider resourceProvider, Comparable<?> comparable) {
+        synchronized (this) {
+            String[] elements = split(prefix, '/');
+            List<ResourceProviderEntry> entryPath = new ArrayList<ResourceProviderEntry>();
+            populateProviderPath(entryPath, elements);
+            if (entryPath.size() > 0 && entryPath.size() == elements.length) {
+                // the last element is a perfect match;
+                return entryPath.get(entryPath.size()-1).removeInternalProvider(new WrappedResourceProvider(resourceProvider, comparable));
+            }
+            return false;
+        }
+    }
+
+    // ---------- Comparable<ResourceProviderEntry> interface ------------------
+
+    public int compareTo(ResourceProviderEntry o) {
+        return prefix.compareTo(o.prefix);
+    }
+
+    // ---------- internal -----------------------------------------------------
+
+    /**
+     * Adds a list of providers to this entry.
+     *
+     * @param provider
+     */
+    private boolean addInternalProvider(WrappedResourceProvider provider) {
+        synchronized (providers) {
+            int before = providers.length;
+            Set<WrappedResourceProvider> set = new HashSet<WrappedResourceProvider>();
+            if (providers != null) {
+                set.addAll(Arrays.asList(providers));
+            }
+            LOGGER.debug("Adding provider {} at {} ",provider,path);
+            set.add(provider);
+            providers = conditionalSort(set);
+            return providers.length > before;
+        }
+
+    }
+
+    /**
+     * @param provider
+     * @return
+     */
+    private boolean removeInternalProvider(WrappedResourceProvider provider) {
+        synchronized (providers) {
+            int before = providers.length;
+            Set<WrappedResourceProvider> set = new HashSet<WrappedResourceProvider>();
+            if (providers != null) {
+                set.addAll(Arrays.asList(providers));
+            }
+            set.remove(provider);
+            providers = conditionalSort(set);
+            return providers.length < before;
+        }
+    }
+
+    /**
+     * @param set
+     * @return
+     */
+    private WrappedResourceProvider[] conditionalSort(Set<WrappedResourceProvider> set) {
+
+        List<WrappedResourceProvider> providerList = new ArrayList<WrappedResourceProvider>(
+                set);
+
+        Collections.sort(providerList, new Comparator<WrappedResourceProvider>() {
+
+            @SuppressWarnings("unchecked")
+            public int compare(WrappedResourceProvider o1, WrappedResourceProvider o2) {
+                Comparable c1 = o1.getComparable();
+                Comparable c2 = o2.getComparable();
+                if ( c1 == null && c2 == null ) {
+                  return 0;
+                }
+                if ( c1 == null ) {
+                  return -1;
+                }
+                if ( c2 == null ) {
+                  return 1;
+                }
+                return c1.compareTo(c2);
+            }
+        });
+
+        return set.toArray(new WrappedResourceProvider[set.size()]);
+    }
+
+    /**
+     * Get a of ResourceProvidersEntries leading to the fullPath in reverse
+     * order.
+     *
+     * @param fullPath
+     *            the full path
+     */
+    private void populateProviderPath(
+        List<ResourceProviderEntry> providerEntryPath, String[] elements) {
+        ResourceProviderEntry base = this;
+        if (elements != null) {
+            for (String element : elements) {
+                if (element != null) {
+                    if (base.containsKey(element)) {
+                        base = base.get(element);
+                        providerEntryPath.add(base);
+                    } else {
+                        break;
+                    }
+                }
+            }
+        }
+    }
+
+
+    /**
+     * Resolve a resource from a path into a Resource
+     *
+     * @param resolver
+     *            the ResourceResolver.
+     * @param fullPath
+     *            the Full path
+     * @return null if no resource was found, a resource if one was found.
+     */
+    private Resource getInternalResource(ResourceResolver resourceResolver,
+            String fullPath) {
+        long start = System.currentTimeMillis();
+        try {
+
+            if (fullPath == null || fullPath.length() == 0
+                    || fullPath.charAt(0) != '/') {
+                nmiss++;
+                LOGGER.debug("Not absolute {} :{}",fullPath,(System.currentTimeMillis() - start));
+                return null; // fullpath must be absolute
+            }
+            String[] elements = split(fullPath, '/');
+
+            List<ResourceProviderEntry> list = new ArrayList<ResourceProviderEntry>();
+            populateProviderPath(list, elements);
+            // the path is in reverse order end first
+
+            for(int i = list.size()-1; i >= 0; i--) {
+                ResourceProvider[] rps = list.get(i).getResourceProviders();
+                for (ResourceProvider rp : rps) {
+
+                    Resource resource = rp.getResource(resourceResolver,
+                            fullPath);
+                    if (resource != null) {
+                        nreal++;
+                        LOGGER.debug("Resolved Full {} using {} from {} ",new Object[]{
+                                fullPath, rp, Arrays.toString(rps)});
+                        return resource;
+                    }
+                }
+            }
+
+            // resolve against this one
+            final Resource resource = getResourceFromProviders(
+                resourceResolver, fullPath);
+            if (resource != null) {
+                return resource;
+            }
+
+            // query: /libs/sling/servlet/default
+            // resource Provider: libs/sling/servlet/default/GET.servlet
+            // list will match libs, sling, servlet, default
+            // and there will be no resource provider at the end
+            if (list.size() > 0 && list.size() == elements.length ) {
+                if ( list.get(list.size()-1).getResourceProviders().length == 0 ) {
+                    nsynthetic++;
+                    LOGGER.debug("Resolved Synthetic {}", fullPath);
+                    return new SyntheticResource(resourceResolver,
+                            fullPath,
+                            ResourceProvider.RESOURCE_TYPE_SYNTHETIC);
+                }
+            }
+
+
+
+            LOGGER.debug("Resource null {} ", fullPath);
+            nmiss++;
+            return null;
+        } catch (Exception ex) {
+            LOGGER.debug("Failed! ",ex);
+            return null;
+        } finally {
+            ttime += System.currentTimeMillis() - start;
+        }
+    }
+
+    Resource getResourceFromProviders(final ResourceResolver resourceResolver,
+            final String fullPath) {
+        ResourceProvider[] rps = getResourceProviders();
+        for (ResourceProvider rp : rps) {
+            Resource resource = rp.getResource(resourceResolver, fullPath);
+            if (resource != null) {
+                nreal++;
+                LOGGER.debug("Resolved Base {} using {} ", fullPath, rp);
+                return resource;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * @param st
+     * @param sep
+     * @return an array of the strings between the separator
+     */
+    static String[] split(String st, char sep) {
+
+        if (st == null) {
+            return new String[0];
+        }
+        char[] pn = st.toCharArray();
+        if (pn.length == 0) {
+            return new String[0];
+        }
+        if (pn.length == 1 && pn[0] == sep) {
+            return new String[0];
+        }
+        int n = 1;
+        int start = 0;
+        int end = pn.length;
+        while (start < end && sep == pn[start])
+            start++;
+        while (start < end && sep == pn[end - 1])
+            end--;
+        for (int i = start; i < end; i++) {
+            if (sep == pn[i]) {
+                n++;
+            }
+        }
+        String[] e = new String[n];
+        int s = start;
+        int j = 0;
+        for (int i = start; i < end; i++) {
+            if (pn[i] == sep) {
+                e[j++] = new String(pn, s, i - s);
+                s = i + 1;
+            }
+        }
+        if (s < end) {
+            e[j++] = new String(pn, s, end - s);
+        }
+        return e;
+    }
+
+    public String getResolutionStats() {
+        long tot = nreal + nsynthetic + nmiss;
+        if (tot == 0) {
+            return null;
+        }
+        float n = tot;
+        float t = ttime;
+        float persec = 1000 * n / t;
+        float avgtime = t / n;
+
+        String stat = "Resolved: Real(" + nreal + ") Synthetic(" + nsynthetic
+                + ") Missing(" + nmiss + ") Total(" + tot + ") at " + persec
+                + " ops/sec avg " + avgtime + " ms";
+        ttime = nmiss = nsynthetic = nreal = 0L;
+        return stat;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @see java.util.AbstractMap#toString()
+     */
+    @Override
+    public String toString() {
+        return this.path;
+        //"{path:\"" + this.path + "\", providers:"+Arrays.toString(getResourceProviders())+", map:" + storageMap.toString() + "}";
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/tree/RootResourceProviderEntry.java b/src/main/java/org/apache/sling/resourceresolver/impl/tree/RootResourceProviderEntry.java
new file mode 100644
index 0000000..aeb550d
--- /dev/null
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/tree/RootResourceProviderEntry.java
@@ -0,0 +1,141 @@
+/*
+ * 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.resource.internal.helper;
+
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.Map;
+
+import org.apache.sling.api.SlingConstants;
+import org.apache.sling.api.resource.ResourceProvider;
+import org.apache.sling.commons.osgi.OsgiUtil;
+import org.osgi.framework.Constants;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventAdmin;
+import org.osgi.util.tracker.ServiceTracker;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This is the root resource provider entry which keeps track
+ * of the resource providers.
+ */
+public class RootResourceProviderEntry extends ResourceProviderEntry {
+
+    /** default logger */
+    private final Logger logger = LoggerFactory.getLogger(getClass());
+
+    public RootResourceProviderEntry() {
+        super("/", null);
+    }
+
+    public void bindResourceProvider(final ResourceProvider provider,
+                                     final Map<String, Object> props,
+                                     final ServiceTracker eventAdminTracker) {
+
+        final String serviceName = getServiceName(provider, props);
+
+        logger.debug("bindResourceProvider: Binding {}", serviceName);
+
+        String[] roots = OsgiUtil.toStringArray(props.get(ResourceProvider.ROOTS));
+        if (roots != null && roots.length > 0) {
+            final EventAdmin localEA = (EventAdmin) ( eventAdminTracker != null ? eventAdminTracker.getService() : null);
+
+            for (String root : roots) {
+                // cut off trailing slash
+                if (root.endsWith("/") && root.length() > 1) {
+                    root = root.substring(0, root.length() - 1);
+                }
+
+                // synchronized insertion of new resource providers into
+                // the tree to not inadvertently loose an entry
+                synchronized (this) {
+
+                    this.addResourceProvider(root,
+                        provider, OsgiUtil.getComparableForServiceRanking(props));
+                }
+                logger.debug("bindResourceProvider: {}={} ({})",
+                    new Object[] { root, provider, serviceName });
+                if ( localEA != null ) {
+                    final Dictionary<String, Object> eventProps = new Hashtable<String, Object>();
+                    eventProps.put(SlingConstants.PROPERTY_PATH, root);
+                    localEA.postEvent(new Event(SlingConstants.TOPIC_RESOURCE_PROVIDER_ADDED,
+                            eventProps));
+                }
+            }
+        }
+
+        logger.debug("bindResourceProvider: Bound {}", serviceName);
+    }
+
+    public void unbindResourceProvider(final ResourceProvider provider,
+                                       final Map<String, Object> props,
+                                       final ServiceTracker eventAdminTracker) {
+
+        final String serviceName = getServiceName(provider, props);
+
+        logger.debug("unbindResourceProvider: Unbinding {}", serviceName);
+
+        String[] roots = OsgiUtil.toStringArray(props.get(ResourceProvider.ROOTS));
+        if (roots != null && roots.length > 0) {
+
+            final EventAdmin localEA = (EventAdmin) ( eventAdminTracker != null ? eventAdminTracker.getService() : null);
+
+            for (String root : roots) {
+                // cut off trailing slash
+                if (root.endsWith("/") && root.length() > 1) {
+                    root = root.substring(0, root.length() - 1);
+                }
+
+                // synchronized insertion of new resource providers into
+                // the tree to not inadvertently loose an entry
+                synchronized (this) {
+                    // TODO: Do not remove this path, if another resource
+                    // owns it. This may be the case if adding the provider
+                    // yielded an ResourceProviderEntryException
+                    this.removeResourceProvider(root, provider, OsgiUtil.getComparableForServiceRanking(props));
+                }
+                logger.debug("unbindResourceProvider: root={} ({})", root,
+                    serviceName);
+                if ( localEA != null ) {
+                    final Dictionary<String, Object> eventProps = new Hashtable<String, Object>();
+                    eventProps.put(SlingConstants.PROPERTY_PATH, root);
+                    localEA.postEvent(new Event(SlingConstants.TOPIC_RESOURCE_PROVIDER_REMOVED,
+                            eventProps));
+                }
+            }
+        }
+
+        logger.debug("unbindResourceProvider: Unbound {}", serviceName);
+    }
+
+    private String getServiceName(final ResourceProvider provider, final Map<String, Object> props) {
+        if (logger.isDebugEnabled()) {
+            StringBuilder snBuilder = new StringBuilder(64);
+            snBuilder.append('{');
+            snBuilder.append(provider.toString());
+            snBuilder.append('/');
+            snBuilder.append(props.get(Constants.SERVICE_ID));
+            snBuilder.append('}');
+            return snBuilder.toString();
+        }
+
+        return null;
+    }
+}
\ No newline at end of file

-- 
To stop receiving notification emails like this one, please contact
"commits@sling.apache.org" <co...@sling.apache.org>.