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

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

commit def05b60d30b8dd1a731f77501aa28a6c549f0b7
Author: Robert Munteanu <[email protected]>
AuthorDate: Tue Aug 14 16:05:32 2018 +0200

    SLING-7792 - Resource Resolver should return more than one resolved path if 
available
    
    Add a new ResourceMapperImpl class that contains the logic for fine-grained 
access to
    resource mappings.
    
    Added a new set of tests that validate that the usage of the various 
mapping sources
    (aliases, /etc/map) is correct.
    
    This change also opens up some methods from the ResourceResolverImpl, but 
it looks
    reasonable as they are not exposed outside the bundle.
---
 .../impl/ResourceResolverImpl.java                 | 220 +-----------
 .../impl/mapping/ResourceMapperImpl.java           | 368 +++++++++++++++++++++
 .../impl/mapping/InMemoryResource.java             |  96 ++++++
 .../impl/mapping/InMemoryResourceProvider.java     |  79 +++++
 .../impl/mapping/ResourceMapperImplTest.java       | 267 +++++++++++++++
 5 files changed, 823 insertions(+), 207 deletions(-)

diff --git 
a/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverImpl.java
 
b/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverImpl.java
index 4acf759..21ca9c9 100644
--- 
a/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverImpl.java
+++ 
b/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverImpl.java
@@ -25,7 +25,6 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -48,6 +47,7 @@ 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.ResourceWrapper;
+import org.apache.sling.api.resource.mapping.ResourceMapper;
 import org.apache.sling.resourceresolver.impl.helper.RedirectResource;
 import org.apache.sling.resourceresolver.impl.helper.ResourceIteratorDecorator;
 import org.apache.sling.resourceresolver.impl.helper.ResourcePathIterator;
@@ -57,13 +57,14 @@ import 
org.apache.sling.resourceresolver.impl.helper.StarResource;
 import org.apache.sling.resourceresolver.impl.helper.URI;
 import org.apache.sling.resourceresolver.impl.helper.URIException;
 import org.apache.sling.resourceresolver.impl.mapping.MapEntry;
+import org.apache.sling.resourceresolver.impl.mapping.ResourceMapperImpl;
 import org.apache.sling.resourceresolver.impl.params.ParsedParameters;
 import 
org.apache.sling.resourceresolver.impl.providers.ResourceProviderStorageProvider;
 import org.apache.sling.spi.resource.provider.ResourceProvider;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-@Adaptable(adaptableClass = ResourceResolver.class, adapters = { 
@Adapter(Session.class) })
+@Adaptable(adaptableClass = ResourceResolver.class, adapters = { 
@Adapter(Session.class), @Adapter(ResourceMapper.class) })
 public class ResourceResolverImpl extends SlingAdaptable implements 
ResourceResolver {
 
     /** Default logger */
@@ -79,7 +80,7 @@ public class ResourceResolverImpl extends SlingAdaptable 
implements ResourceReso
     // 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";
+    public static final String JCR_CONTENT_LEAF = "/jcr:content";
 
     /** The factory which created this resource resolver. */
     private final CommonResourceResolverFactoryImpl factory;
@@ -181,7 +182,7 @@ public class ResourceResolverImpl extends SlingAdaptable 
implements ResourceReso
      * @throws IllegalStateException
      *             If the resolver is already closed or the factory is no 
longer live.
      */
-    private void checkClosed() {
+    public void checkClosed() {
         if (this.control.isClosed()) {
             if (closedResolverException != null) {
                 logger.error("The ResourceResolver has already been closed.", 
closedResolverException);
@@ -415,200 +416,7 @@ public class ResourceResolverImpl extends SlingAdaptable 
implements ResourceReso
      */
     @Override
     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);
-
-        }
-
-        ParsedParameters parsed = new ParsedParameters(mappedPath);
-        final Resource nonDecoratedResource = 
resolveInternal(parsed.getRawPath(), parsed.getParameters());
-
-        if (nonDecoratedResource != null) {
-
-            //Invoke the decorator for the resolved resource
-            Resource 
res=this.factory.getResourceDecoratorTracker().decorate(nonDecoratedResource);
-
-            // 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<>();
-
-            Resource current = res;
-            String path = res.getPath();
-            while (path != null) {
-                String alias = null;
-                if (current != null && !path.endsWith(JCR_CONTENT_LEAF)) {
-                    if (factory.isOptimizeAliasResolutionEnabled()) {
-                        logger.debug("map: Optimize Alias Resolution is 
Enabled");
-                        String parentPath = ResourceUtil.getParent(path);
-                        if (parentPath != null) {
-                            final Map<String, String> aliases = 
factory.getMapEntries().getAliasMap(parentPath);
-                            if (aliases!= null && 
aliases.containsValue(current.getName())) {
-                                for (String key:aliases.keySet()) {
-                                    if 
(current.getName().equals(aliases.get(key))) {
-                                        alias = key;
-                                        break;
-                                    }
-                                }
-                            }
-                        }
-                    } else {
-                        logger.debug("map: Optimize Alias Resolution is 
Disabled");
-                        alias = ResourceResolverControl.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("map: 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()
-            final 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 (final 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;
+        return adaptTo(ResourceMapper.class).getMapping(resourcePath, request);
     }
 
     // ---------- search path for relative resoures
@@ -805,6 +613,7 @@ public class ResourceResolverImpl extends SlingAdaptable 
implements ResourceReso
     /**
      * @see 
org.apache.sling.api.adapter.SlingAdaptable#adaptTo(java.lang.Class)
      */
+    @SuppressWarnings("unchecked")
     @Override
     public <AdapterType> AdapterType adaptTo(final Class<AdapterType> type) {
         checkClosed();
@@ -812,6 +621,11 @@ public class ResourceResolverImpl extends SlingAdaptable 
implements ResourceReso
         if (type.getName().equals("javax.jcr.Session")) {
             return getSession(type);
         }
+        
+        if ( type == ResourceMapper.class )
+            return (AdapterType) new ResourceMapperImpl(this, 
factory.getResourceDecoratorTracker(), factory.getMapEntries(), 
+                    factory.isOptimizeAliasResolutionEnabled(), 
factory.getNamespaceMangler());
+        
         final AdapterType result = this.control.adaptTo(this.context, type);
         if ( result != null ) {
             return result;
@@ -873,7 +687,7 @@ public class ResourceResolverImpl extends SlingAdaptable 
implements ResourceReso
      *         the part of the <code>absPath</code> which has been cut off by
      *         the {@link ResourcePathIterator} to resolve the resource.
      */
-    private Resource resolveInternal(final String absPath, final Map<String, 
String> parameters) {
+    public Resource resolveInternal(final String absPath, final Map<String, 
String> parameters) {
         Resource resource = null;
         String curPath = absPath;
         try {
@@ -1102,14 +916,6 @@ public class ResourceResolverImpl extends SlingAdaptable 
implements ResourceReso
         return path;
     }
 
-    private String mangleNamespaces(String absPath) {
-        if ( absPath != null && factory.getNamespaceMangler() != null ) {
-            absPath = 
((JcrNamespaceMangler)factory.getNamespaceMangler()).mangleNamespaces(this, 
logger, absPath);
-        }
-
-        return absPath;
-    }
-
     private String unmangleNamespaces(String absPath) {
         if (absPath != null && factory.getNamespaceMangler() != null ) {
             absPath = 
((JcrNamespaceMangler)factory.getNamespaceMangler()).unmangleNamespaces(this, 
logger, absPath);
diff --git 
a/src/main/java/org/apache/sling/resourceresolver/impl/mapping/ResourceMapperImpl.java
 
b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/ResourceMapperImpl.java
new file mode 100644
index 0000000..5610054
--- /dev/null
+++ 
b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/ResourceMapperImpl.java
@@ -0,0 +1,368 @@
+/*
+ * 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.resourceresolver.impl.mapping;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.UnaryOperator;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.api.resource.mapping.ResourceMapper;
+import org.apache.sling.resourceresolver.impl.JcrNamespaceMangler;
+import org.apache.sling.resourceresolver.impl.ResourceResolverImpl;
+import org.apache.sling.resourceresolver.impl.helper.ResourceDecoratorTracker;
+import org.apache.sling.resourceresolver.impl.helper.ResourceResolverControl;
+import org.apache.sling.resourceresolver.impl.helper.URI;
+import org.apache.sling.resourceresolver.impl.helper.URIException;
+import org.apache.sling.resourceresolver.impl.params.ParsedParameters;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ResourceMapperImpl implements ResourceMapper {
+    
+    private final Logger logger = LoggerFactory.getLogger(getClass());
+
+    private final ResourceResolverImpl resolver;
+    private final ResourceDecoratorTracker resourceDecorator;
+    private final MapEntriesHandler mapEntries;
+    private final boolean optimizedAliasResolutionEnabled;
+    private final Object namespaceMangler;
+    
+
+    public ResourceMapperImpl(ResourceResolverImpl resolver, 
ResourceDecoratorTracker resourceDecorator, 
+            MapEntriesHandler mapEntries, boolean 
optimizedAliasResolutionEnabled, Object namespaceMangler) {
+        this.resolver = resolver;
+        this.resourceDecorator = resourceDecorator;
+        this.mapEntries = mapEntries;
+        this.optimizedAliasResolutionEnabled = optimizedAliasResolutionEnabled;
+        this.namespaceMangler = namespaceMangler;
+    }
+
+    @Override
+    public String getMapping(String resourcePath) {
+        return getMapping(resourcePath, null);
+    }
+
+    @Override
+    public String getMapping(String resourcePath, HttpServletRequest request) {
+        
+        Collection<String> mappings = getAllMappings(resourcePath, request);
+        if ( mappings.isEmpty() )
+            return null;
+        
+        return mappings.iterator().next();
+    }
+
+    @Override
+    public Collection<String> getAllMappings(String resourcePath) {
+        return getAllMappings(resourcePath, null);
+    }
+
+    @Override
+    public Collection<String> getAllMappings(String resourcePath, 
HttpServletRequest request) {
+        
+        resolver.checkClosed();
+        
+        List<String> mappings = new ArrayList<>();
+        
+        // 1. parse parameters
+        
+        // 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;
+        }
+
+        final RequestContext requestContext = new RequestContext(request, 
resourcePath);
+        ParsedParameters parsed = new ParsedParameters(mappedPath);
+        
+        // 2. add the requested path itself
+        mappings.add(mappedPath);
+
+        
+        // 3. load aliases
+        final Resource nonDecoratedResource = 
resolver.resolveInternal(parsed.getRawPath(), parsed.getParameters());
+        if (nonDecoratedResource != null) {
+            loadAliasIfApplicable(mappings, nonDecoratedResource);
+        }
+
+        // 4. load /etc/map entries
+        // populate entries from all entries, including original path and any 
found aliases
+        for ( String mapped : new ArrayList<>(mappings) )
+            populateMappingsFromMapEntries(mappings, mapped, requestContext);
+
+        
+        // 5. apply context path if needed
+        mappings.replaceAll(new ApplyContextPath(request));
+       
+        // 6. set back the fragment query if needed
+        if ( fragmentQuery != null ) {
+            mappings.replaceAll(new UnaryOperator<String>() {
+                @Override
+                public String apply(String mappedPath) {
+                        return mappedPath.concat(fragmentQuery);
+                }
+            });
+        }
+
+        mappings.forEach( path -> {
+            logger.debug("map: Returning URL {} as mapping for path {}", path, 
resourcePath);    
+        });
+        
+        
+        // The API contract of the ResourceMapper does not specify the order 
in which the elements are returned
+        // As an implementation detail however the getMapping method picks the 
first element of the return value
+        // as the 'winner'.
+        //
+        // Therefore we reverse the sorting to preserve the logic of the old 
ResourceResolver.map() method (last
+        // found wins) and also make sure that no duplicates are added
+        //
+        // There is some room for improvement here by using a data structure 
that does not need reversing ( ArrayList
+        // .add moves the elements every time ) or reversal of duplicates but 
the expectation is that we have a small
+        // number of mappings ( <= 10 ) so the time spent here should be 
negligible.
+        
+        Collections.reverse(mappings);
+        
+        return new LinkedHashSet<>(mappings);
+    }
+
+    private void loadAliasIfApplicable(List<String> mappings, final Resource 
nonDecoratedResource) {
+        //Invoke the decorator for the resolved resource
+        Resource res = resourceDecorator.decorate(nonDecoratedResource);
+
+        // 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<>();
+
+        Resource current = res;
+        String path = res.getPath();
+        while (path != null) {
+            String alias = null;
+            if (current != null && 
!path.endsWith(ResourceResolverImpl.JCR_CONTENT_LEAF)) {
+                if (optimizedAliasResolutionEnabled) {
+                    logger.debug("map: Optimize Alias Resolution is Enabled");
+                    String parentPath = ResourceUtil.getParent(path);
+                    if (parentPath != null) {
+                        final Map<String, String> aliases = 
mapEntries.getAliasMap(parentPath);
+                        if (aliases!= null && 
aliases.containsValue(current.getName())) {
+                            for (String key:aliases.keySet()) {
+                                if 
(current.getName().equals(aliases.get(key))) {
+                                    alias = key;
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                } else {
+                    logger.debug("map: Optimize Alias Resolution is Disabled");
+                    alias = ResourceResolverControl.getProperty(current, 
ResourceResolverImpl.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
+        String mappedPath = buf.toString();
+
+        logger.debug("map: Alias mapping resolves to path {}", mappedPath);
+        
+        mappings.add(mappedPath);
+    }
+
+    private void populateMappingsFromMapEntries(List<String> mappings, String 
mappedPath,
+            final RequestContext requestContext) {
+        boolean mappedPathIsUrl = false;
+        for (final MapEntry mapEntry : mapEntries.getMapMaps()) {
+            final String[] mappedPaths = mapEntry.replace(mappedPath);
+            if (mappedPaths != null) {
+
+                logger.debug("map: Match for Entry {}", mapEntry);
+
+                mappedPathIsUrl = !mapEntry.isInternal();
+
+                if (mappedPathIsUrl && requestContext.hasUri() ) {
+
+                    mappedPath = null;
+
+                    for (final String candidate : mappedPaths) {
+                        if (candidate.startsWith(requestContext.getUri())) {
+                            mappedPath = 
candidate.substring(requestContext.getUri().length() - 1);
+                            mappedPathIsUrl = false;
+                            logger.debug("map: Found host specific mapping {} 
resolving to {}", candidate, mappedPath);
+                            break;
+                        } else if 
(candidate.startsWith(requestContext.getSchemeWithPrefix()) && 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("map: MapEntry {} matches, mapped path is {}", 
mapEntry, mappedPath);
+                
+                mappings.add(mappedPath);
+
+                break;
+            }
+        }
+    }
+    
+    private String mangleNamespaces(String absPath) {
+        if ( absPath != null && namespaceMangler != null && namespaceMangler 
instanceof JcrNamespaceMangler ) {
+            absPath = ((JcrNamespaceMangler) 
namespaceMangler).mangleNamespaces(resolver, logger, absPath);
+        }
+
+        return absPath;
+    }
+    
+    private class RequestContext {
+        
+        private final String uri;
+        private final String schemeWithPrefix;
+        
+        private RequestContext(HttpServletRequest request, String 
resourcePath) {
+            if ( request != null ) {
+                this.uri = MapEntry.getURI(request.getScheme(), 
request.getServerName(), request.getServerPort(), "/");
+                this.schemeWithPrefix = request.getScheme().concat("://");
+                logger.debug("map: Mapping path {} for {} (at least with 
scheme prefix {})", new Object[] { resourcePath,
+                        uri, schemeWithPrefix });
+            } else {
+                this.uri = null;
+                this.schemeWithPrefix = null;
+                logger.debug("map: Mapping path {} for default", resourcePath);
+            }
+            
+        }
+        
+        public String getUri() {
+            return uri;
+        }
+        
+        public String getSchemeWithPrefix() {
+            return schemeWithPrefix;
+        }
+        
+        public boolean hasUri() {
+            return uri != null && schemeWithPrefix != null;
+        }
+    }
+    
+    private class ApplyContextPath implements UnaryOperator<String> {
+        
+        private final HttpServletRequest req;
+        
+        private ApplyContextPath(HttpServletRequest req) {
+            this.req = req;
+        }
+
+        @Override
+        public String apply(String path) {
+            
+            String mappedPath = path;
+            
+            // [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()
+                final URI uri = new URI(path, false);
+
+                // 1. mangle the namespaces in the path
+                path = mangleNamespaces(uri.getPath());
+
+                // 2. prepend servlet context path if we have a request
+                if (req != null && req.getContextPath() != null && 
req.getContextPath().length() > 0) {
+                    path = req.getContextPath().concat(path);
+                }
+                // update the path part of the URI
+                uri.setPath(path);
+
+                mappedPath = uri.toString();
+            } catch (final URIException e) {
+                logger.warn("map: Unable to mangle namespaces for " + 
mappedPath + " returning unmangled", e);
+            }
+
+            return mappedPath;
+        }
+        
+    }
+}
diff --git 
a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/InMemoryResource.java
 
b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/InMemoryResource.java
new file mode 100644
index 0000000..9792ca9
--- /dev/null
+++ 
b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/InMemoryResource.java
@@ -0,0 +1,96 @@
+/*
+ * 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.resourceresolver.impl.mapping;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.sling.api.resource.AbstractResource;
+import org.apache.sling.api.resource.ResourceMetadata;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.api.wrappers.ValueMapDecorator;
+
+public class InMemoryResource extends AbstractResource {
+        
+        private final String path;
+        private final ResourceMetadata metadata;
+        private final ResourceResolver resolver;
+        private final Map<String, Object> properties = new HashMap<>();
+
+        public InMemoryResource(String path, ResourceResolver resolver, 
Map<String, Object> properties) {
+            
+            if ( path == null )
+                throw new IllegalArgumentException("path is null");
+            
+            if ( resolver == null )
+                throw new IllegalArgumentException("resovler is null");
+            
+            this.path = path;
+            this.metadata = new ResourceMetadata();
+            this.metadata.setResolutionPath(path);;
+            this.resolver = resolver;
+            this.properties.putAll(properties);
+        }
+
+        @Override
+        public String getPath() {
+            return path;
+        }
+
+        @Override
+        public String getResourceType() {
+            return getValueMap().get("sling:resourceType", String.class);
+        }
+
+        @Override
+        public String getResourceSuperType() {
+            return getValueMap().get("sling:resourceSuperType", String.class);
+        }
+
+        @Override
+        public ResourceMetadata getResourceMetadata() {
+            return metadata;
+        }
+
+        @Override
+        public ResourceResolver getResourceResolver() {
+            return resolver;
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public <AdapterType> AdapterType adaptTo(Class<AdapterType> type) {
+            if(type == ValueMap.class || type == Map.class) {
+                return (AdapterType) new ValueMapDecorator(properties);
+            }
+            return super.adaptTo(type);
+        }
+        
+        public InMemoryResource set(String prop, Object val) {
+            properties.put(prop, val);
+            
+            return this;
+        }
+        
+        @Override
+        public String toString() {
+            return getClass().getSimpleName() + " : [ path = " + path + " ]";
+        }
+}
\ No newline at end of file
diff --git 
a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/InMemoryResourceProvider.java
 
b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/InMemoryResourceProvider.java
new file mode 100644
index 0000000..956a25d
--- /dev/null
+++ 
b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/InMemoryResourceProvider.java
@@ -0,0 +1,79 @@
+/*
+ * 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.resourceresolver.impl.mapping;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.spi.resource.provider.ResolveContext;
+import org.apache.sling.spi.resource.provider.ResourceContext;
+import org.apache.sling.spi.resource.provider.ResourceProvider;
+import org.osgi.service.component.annotations.Component;
+
+@Component(service = ResourceProvider.class)
+public class InMemoryResourceProvider extends ResourceProvider<Void> {
+    
+    private final Map<String, Map<String, Object>> resources = new HashMap<>();
+
+    @Override
+    public Resource getResource(ResolveContext<Void> ctx, String path, 
ResourceContext resourceContext,
+            Resource parent) {
+        
+        Map<String, Object> vals = resources.get(path);
+        if ( vals == null )
+            return null;
+        
+        return new InMemoryResource(path, ctx.getResourceResolver(), vals);
+            
+    }
+
+    @Override
+    public Iterator<Resource> listChildren(ResolveContext<Void> ctx, Resource 
parent) {
+
+        return resources.entrySet().stream()
+            .filter( e -> 
parent.getPath().equals(ResourceUtil.getParent(e.getKey())) )
+            .map( e -> (Resource) new InMemoryResource(e.getKey(), 
ctx.getResourceResolver(), e.getValue()) )
+            .iterator();
+    }
+    
+    public void putResource(String path) {
+        putResource(path, Collections.emptyMap());
+    }
+
+    public void putResource(String path, String key, Object value) {
+        putResource(path, Collections.singletonMap(key, value));
+    }
+
+    public void putResource(String path, String key, Object value, String 
key2, Object value2) {
+        Map<String, Object> props = new HashMap<>();
+        props.put(key, value);
+        props.put(key2, value2);
+        putResource(path, props);
+    }
+    
+    public void putResource(String path, Map<String, Object> props) {
+        resources.put(path, props);
+    }
+    
+}
\ No newline at end of file
diff --git 
a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/ResourceMapperImplTest.java
 
b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/ResourceMapperImplTest.java
new file mode 100644
index 0000000..6caefa9
--- /dev/null
+++ 
b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/ResourceMapperImplTest.java
@@ -0,0 +1,267 @@
+/*
+ * 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.resourceresolver.impl.mapping;
+
+import static 
org.apache.sling.resourceresolver.impl.ResourceResolverImpl.PROP_ALIAS;
+import static 
org.apache.sling.spi.resource.provider.ResourceProvider.PROPERTY_NAME;
+import static 
org.apache.sling.spi.resource.provider.ResourceProvider.PROPERTY_ROOT;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.servlet.http.HttpServletRequest;
+
+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.ResourceResolverFactory;
+import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.api.resource.mapping.ResourceMapper;
+import org.apache.sling.resourceresolver.impl.ResourceAccessSecurityTracker;
+import org.apache.sling.resourceresolver.impl.ResourceResolverFactoryActivator;
+import org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl;
+import org.apache.sling.spi.resource.provider.ResourceProvider;
+import org.apache.sling.testing.mock.osgi.junit.OsgiContext;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+/**
+ * Validates that the {@link ResourceMapperImpl} correctly queries all sources 
of mappings
+ * 
+ * <p>This test ensures that in case more than one mappings is possible, for 
instance:
+ * 
+ * <ol>
+ *   <li>path for an existing resource</li>
+ *   <li>alias</li>
+ *   <li>/etc/map entries</li>
+ * </ol>
+ * 
+ * all are correctly considered and included in the relevant method calls.
+ * </p>
+ * 
+ * <p>This test should not exhaustively test all mapping scenarios, other 
tests in this
+ * module and the Sling ITs cover that.</p>
+ *
+ */
+public class ResourceMapperImplTest {
+    
+    @Rule
+    public final OsgiContext ctx = new OsgiContext();
+
+    private HttpServletRequest req;
+    private ResourceResolver resolver;
+
+    @Before
+    public void prepare() throws LoginException {
+
+        ctx.registerInjectActivateService(new ServiceUserMapperImpl());
+        ctx.registerInjectActivateService(new ResourceAccessSecurityTracker());
+        
+        InMemoryResourceProvider resourceProvider = new 
InMemoryResourceProvider();
+        resourceProvider.putResource("/"); // root
+        resourceProvider.putResource("/here"); // regular page
+        resourceProvider.putResource("/there", PROP_ALIAS, "alias-value"); // 
with alias
+        resourceProvider.putResource("/somewhere", PROP_ALIAS, 
"alias-value-2"); // with alias and also /etc/map
+        
+        // build /etc/map structure
+        resourceProvider.putResource("/etc");
+        resourceProvider.putResource("/etc/map");
+        resourceProvider.putResource("/etc/map/http");
+        resourceProvider.putResource("/etc/map/http/localhost_any",
+                "sling:internalRedirect", "/somewhere",
+                "sling:match", "localhost.8080/everywhere");
+        
+        // we fake the fact that we are the JCR resource provider since it's 
the required one
+        ctx.registerService(ResourceProvider.class, resourceProvider, 
PROPERTY_ROOT, "/", PROPERTY_NAME, "JCR");
+        // disable optimised alias resolution as it relies on JCR queries
+        ctx.registerInjectActivateService(new 
ResourceResolverFactoryActivator(),
+                "resource.resolver.optimize.alias.resolution", false);
+        
+        ResourceResolverFactory factory = 
ctx.getService(ResourceResolverFactory.class);
+        
+        assertNotNull(factory);
+        
+        resolver = factory.getResourceResolver(null);
+        
+        req = mock(HttpServletRequest.class);
+        when(req.getScheme()).thenReturn("http");
+        when(req.getServerName()).thenReturn("localhost");
+        when(req.getServerPort()).thenReturn(8080);
+        when(req.getContextPath()).thenReturn("/app");
+    }
+    
+    @After
+    public void cleanup() {
+        if ( resolver != null )
+            resolver.close();
+    }
+    
+    /**
+     * Validates that mappings for a non-existing resource only contain that 
resource's path
+     * 
+     * @throws LoginException
+     */
+    @Test
+    public void mapNonExistingPath() throws LoginException {
+        
+        ExpectedMappings.nonExistingResource("/not-here")
+            .singleMapping("/not-here")
+            .singleMappingWithRequest("/app/not-here")
+            .allMappings("/not-here")
+            .allMappingsWithRequest("/app/not-here")
+            .verify(resolver, req);
+    }
+    
+    /**
+     * Validates that mappings for an existing resource only contain that 
resource's path
+     * 
+     * @throws LoginException
+     */
+    @Test
+    public void mapExistingPath() throws LoginException {
+        
+        ExpectedMappings.existingResource("/here")
+            .singleMapping("/here")
+            .singleMappingWithRequest("/app/here")
+            .allMappings("/here")
+            .allMappingsWithRequest("/app/here")
+            .verify(resolver, req);
+    }
+
+    /**
+     * Validates that mappings for a existing resource with an alias contain 
the alias and the resource's path
+     * 
+     * @throws LoginException
+     */
+    @Test
+    public void mapResourceWithAlias() {
+        
+        ExpectedMappings.existingResource("/there")
+            .singleMapping("/alias-value")
+            .singleMappingWithRequest("/app/alias-value")
+            .allMappings("/alias-value", "/there")
+            .allMappingsWithRequest("/app/alias-value", "/app/there")
+            .verify(resolver, req);
+    }
+
+    /**
+     * Validates that mappings for a existing resource with an alias and 
/etc/map entry
+     * contain the /etc/map entry, the alias and the resource's path
+     * 
+     * @throws LoginException
+     */    
+    @Test
+    public void mapResourceWithAliasAndEtcMap() {
+        
+        ExpectedMappings.existingResource("/somewhere")
+            .singleMapping("http://localhost:8080/everywhere";)
+            .singleMappingWithRequest("/app/everywhere")
+            .allMappings("http://localhost:8080/everywhere";, "/alias-value-2", 
"/somewhere")
+            .allMappingsWithRequest("/app/everywhere","/app/alias-value-2", 
"/app/somewhere")
+            .verify(resolver, req);
+    }
+
+    static class ExpectedMappings {
+        
+        public static ExpectedMappings existingResource(String path) {
+            return new ExpectedMappings(path, true);
+        }
+        
+        public static ExpectedMappings nonExistingResource(String path) {
+            return new ExpectedMappings(path, false);
+        }
+        
+        private final String path;
+        private final boolean exists;
+        
+        private String singleMapping;
+        private String singleMappingWithRequest;
+        private Set<String> allMappings;
+        private Set<String> allMappingsWithRequest;
+        
+        private ExpectedMappings(String path, boolean exists) {
+            this.path = path;
+            this.exists = exists;
+        }
+
+        public ExpectedMappings singleMapping(String singleMapping) {
+            this.singleMapping = singleMapping;
+            
+            return this;
+        }
+        
+        public ExpectedMappings singleMappingWithRequest(String 
singleMappingWithRequest) {
+            this.singleMappingWithRequest = singleMappingWithRequest;
+            
+            return this;
+        }
+        
+        public ExpectedMappings allMappings(String... allMappings) {
+            this.allMappings = new HashSet<>(Arrays.asList(allMappings));
+            
+            return this;
+        }
+
+        public ExpectedMappings allMappingsWithRequest(String... 
allMappingsWithRequest) {
+            this.allMappingsWithRequest = new 
HashSet<>(Arrays.asList(allMappingsWithRequest));
+            
+            return this;
+        }
+        
+        public void verify(ResourceResolver resolver, HttpServletRequest 
request) {
+            checkConfigured();
+            
+            Resource res = resolver.getResource(path);
+            if ( exists ) {
+                assertThat("Resource was null but should exist", res, 
notNullValue());
+                assertThat("Resource is non-existing but should exist", 
ResourceUtil.isNonExistingResource(res), is(false));
+            } else {
+                assertThat("Resource is neither null nor non-existing", res == 
null || ResourceUtil.isNonExistingResource(res), is(true));
+            }
+
+            // downcast to ensure we're testing the right class
+            ResourceMapperImpl mapper = (ResourceMapperImpl) 
resolver.adaptTo(ResourceMapper.class);
+            
+            assertThat("Single mapping without request", 
mapper.getMapping(path), is(singleMapping));
+            assertThat("Single mapping with request", mapper.getMapping(path, 
request), is(singleMappingWithRequest));
+            assertThat("All mappings without request", 
mapper.getAllMappings(path), is(allMappings));
+            assertThat("All mappings with request", 
mapper.getAllMappings(path, request), is(allMappingsWithRequest));
+        }
+
+        private void checkConfigured() {
+            if ( singleMapping == null )
+                throw new IllegalStateException("singleMapping is null");
+            if ( singleMappingWithRequest == null )
+                throw new IllegalStateException("singleMappingWithRequest is 
null");
+            if ( allMappings == null )
+                throw new IllegalStateException("allMappings is null");
+            if ( allMappingsWithRequest == null )
+                throw new IllegalStateException("allMappingsWithRequest is 
null");
+        }
+    }
+}

Reply via email to