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(" <input type='submit' name='" + ATTR_SUBMIT
+ + "' value='Resolve' class='submit'>");
+ pw.println(" <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'> </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'> </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<Resource></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 = <scheme>:<scheme-specific-part>
+ * Generic URI = <scheme>://<authority><path>?<query>
+ * - 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 = <scheme>:<scheme-specific-part>#
+ * <fragment>.
+ *
+ * @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 = <scheme>:<path>?<query>#<
+ * fragment> and relative URI = <path>?<query>#<fragment
+ * >.
+ *
+ * @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 = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
+ * </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 & 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 | "A" | "B" | "C" | "D" | "E" | "F" | "a" | "b" | "c" | "d" | "e"
+ * | "f"
+ * </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 = "-" | "_" | "." | "!" | "˜" | "*" | "'" | "(" | ")"
+ * </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 = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | "$" | ","
+ * </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 | ":" | "@" | "&" | "=" | "+" | "$" | ","
+ * </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 | ";" | "?" | ":" | "@" | "&" | "=" | "+"
+ * | "$" | ","
+ * </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 |
+ * ";" | ":" | "&" | "=" | "+" | "$" | "," )
+ * </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 | "$" | "," | ";" | ":" | "@" | "&" | "=" | "+")
+ * </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 | "+" | "-" | ".")
+ * </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 | ";" | "@" | "&" | "=" | "+" | "$" | ",")
+ * </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
+ * ("&", "=", "+", ",", 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>.