SLING-9662 Introduce Resource Mapping SPI
diff --git a/pom.xml b/pom.xml
index b056ab6..118a877 100644
--- a/pom.xml
+++ b/pom.xml
@@ -106,7 +106,7 @@
         <dependency>
             <groupId>org.apache.sling</groupId>
             <artifactId>org.apache.sling.api</artifactId>
-            <version>2.21.0</version>
+            <version>2.23.0-SNAPSHOT</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/CommonResourceResolverFactoryImpl.java b/src/main/java/org/apache/sling/resourceresolver/impl/CommonResourceResolverFactoryImpl.java
index b925e65..4e423c2 100644
--- a/src/main/java/org/apache/sling/resourceresolver/impl/CommonResourceResolverFactoryImpl.java
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/CommonResourceResolverFactoryImpl.java
@@ -31,12 +31,11 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicBoolean;
 
-import org.jetbrains.annotations.NotNull;
-
 import org.apache.commons.collections4.BidiMap;
 import org.apache.sling.api.resource.LoginException;
 import org.apache.sling.api.resource.ResourceResolver;
 import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.api.resource.mapping.PathToUriMappingService;
 import org.apache.sling.api.resource.path.Path;
 import org.apache.sling.resourceresolver.impl.console.ResourceResolverWebConsolePlugin;
 import org.apache.sling.resourceresolver.impl.helper.ResourceDecoratorTracker;
@@ -48,6 +47,7 @@
 import org.apache.sling.resourceresolver.impl.providers.ResourceProviderTracker;
 import org.apache.sling.serviceusermapping.ServiceUserMapper;
 import org.apache.sling.spi.resource.provider.ResourceProvider;
+import org.jetbrains.annotations.NotNull;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
 import org.slf4j.Logger;
@@ -405,6 +405,11 @@
         return this.activator.getResourceAccessSecurityTracker();
     }
 
+    /** gets PathToUriMappingServiceImpl */
+    public PathToUriMappingService getPathToUriMappingService() {
+        return this.activator.getPathToUriMappingService();
+    }
+
     @NotNull
     @Override
     public ResourceResolver getServiceResourceResolver(
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverFactoryActivator.java b/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverFactoryActivator.java
index 646a077..6de579a 100644
--- a/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverFactoryActivator.java
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverFactoryActivator.java
@@ -34,9 +34,11 @@
 import org.apache.commons.collections4.bidimap.TreeBidiMap;
 import org.apache.sling.api.resource.ResourceDecorator;
 import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.api.resource.mapping.PathToUriMappingService;
 import org.apache.sling.api.resource.path.Path;
 import org.apache.sling.api.resource.runtime.RuntimeService;
 import org.apache.sling.resourceresolver.impl.helper.ResourceDecoratorTracker;
+import org.apache.sling.resourceresolver.impl.mapping.MapEntries;
 import org.apache.sling.resourceresolver.impl.mapping.Mapping;
 import org.apache.sling.resourceresolver.impl.mapping.StringInterpolationProvider;
 import org.apache.sling.resourceresolver.impl.observation.ResourceChangeListenerWhiteboard;
@@ -119,6 +121,9 @@
     @Reference
     ResourceAccessSecurityTracker resourceAccessSecurityTracker;
 
+    @Reference
+    PathToUriMappingService pathToUriMappingService;
+
     volatile ResourceProviderTracker resourceProviderTracker;
 
     volatile ResourceChangeListenerWhiteboard changeListenerWhiteboard;
@@ -153,6 +158,10 @@
         return this.resourceAccessSecurityTracker;
     }
 
+    public PathToUriMappingService getPathToUriMappingService() {
+        return this.pathToUriMappingService;
+    }
+
     public EventAdmin getEventAdmin() {
         return this.eventAdmin;
     }
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 2fddf65..6a565b4 100644
--- a/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverImpl.java
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverImpl.java
@@ -30,11 +30,10 @@
 import java.util.Set;
 import java.util.StringTokenizer;
 
-import org.apache.commons.lang3.StringUtils;
-import org.jetbrains.annotations.Nullable;
 import javax.jcr.Session;
 import javax.servlet.http.HttpServletRequest;
 
+import org.apache.commons.lang3.StringUtils;
 import org.apache.sling.adapter.annotations.Adaptable;
 import org.apache.sling.adapter.annotations.Adapter;
 import org.apache.sling.api.SlingException;
@@ -48,6 +47,7 @@
 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.PathToUriMappingService;
 import org.apache.sling.api.resource.mapping.ResourceMapper;
 import org.apache.sling.resourceresolver.impl.helper.RedirectResource;
 import org.apache.sling.resourceresolver.impl.helper.ResourceIteratorDecorator;
@@ -62,6 +62,7 @@
 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.jetbrains.annotations.Nullable;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -625,7 +626,7 @@
         
         if ( type == ResourceMapper.class )
             return (AdapterType) new ResourceMapperImpl(this, factory.getResourceDecoratorTracker(), factory.getMapEntries(), 
-                    factory.isOptimizeAliasResolutionEnabled(), factory.getNamespaceMangler());
+                    factory.isOptimizeAliasResolutionEnabled(), factory.getNamespaceMangler(), factory.getPathToUriMappingService());
         
         final AdapterType result = this.control.adaptTo(this.context, type);
         if ( result != null ) {
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
index bc47c26..deda902 100644
--- a/src/main/java/org/apache/sling/resourceresolver/impl/mapping/ResourceMapperImpl.java
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/ResourceMapperImpl.java
@@ -33,7 +33,9 @@
 
 import org.apache.sling.api.resource.Resource;
 import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.api.resource.mapping.PathToUriMappingService;
 import org.apache.sling.api.resource.mapping.ResourceMapper;
+import org.apache.sling.api.uri.SlingUri;
 import org.apache.sling.resourceresolver.impl.JcrNamespaceMangler;
 import org.apache.sling.resourceresolver.impl.ResourceResolverImpl;
 import org.apache.sling.resourceresolver.impl.helper.ResourceDecoratorTracker;
@@ -53,15 +55,17 @@
     private final MapEntriesHandler mapEntries;
     private final boolean optimizedAliasResolutionEnabled;
     private final Object namespaceMangler;
-    
+    private final PathToUriMappingService pathToUriMappingService;
 
     public ResourceMapperImpl(ResourceResolverImpl resolver, ResourceDecoratorTracker resourceDecorator, 
-            MapEntriesHandler mapEntries, boolean optimizedAliasResolutionEnabled, Object namespaceMangler) {
+            MapEntriesHandler mapEntries, boolean optimizedAliasResolutionEnabled, Object namespaceMangler,
+            PathToUriMappingService pathToUriMappingService) {
         this.resolver = resolver;
         this.resourceDecorator = resourceDecorator;
         this.mapEntries = mapEntries;
         this.optimizedAliasResolutionEnabled = optimizedAliasResolutionEnabled;
         this.namespaceMangler = namespaceMangler;
+        this.pathToUriMappingService = pathToUriMappingService;
     }
 
     @Override
@@ -166,6 +170,13 @@
         // vanity paths are prepended to make sure they get returned last
         mappings.addAll(0, vanityPaths);
 
+
+        // Apply mappings from Resource Mappers
+        PathToUriMappingService.Result mappingResult = pathToUriMappingService.map(request, mappings.get(mappings.size() - 1));
+        for (Map.Entry<String, SlingUri> mappingFromChain : mappingResult.getIntermediateMappings().entrySet()) {
+            mappings.add(mappingFromChain.getValue().toString());
+        }
+
         // 7. apply context path if needed
         mappings.replaceAll(new ApplyContextPath(request));
        
@@ -174,10 +185,10 @@
             mappings.replaceAll(path -> path.concat(fragmentQuery));
         }
 
-        mappings.forEach( path ->
-            logger.debug("map: Returning URL {} as mapping for path {}", path, resourcePath)    
-        );
-        
+        mappings.forEach( path -> {
+            logger.debug("map: Returning URL {} as mapping for path {}", path, resourcePath);
+        });
+
         Collections.reverse(mappings);
         
         return new LinkedHashSet<>(mappings);
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/urimapping/MappingChainContextInternal.java b/src/main/java/org/apache/sling/resourceresolver/impl/urimapping/MappingChainContextInternal.java
new file mode 100644
index 0000000..8ad49fa
--- /dev/null
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/urimapping/MappingChainContextInternal.java
@@ -0,0 +1,71 @@
+/*
+ * 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.urimapping;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.uri.SlingUri;
+import org.apache.sling.spi.urimapping.MappingChainContext;
+
+class MappingChainContextInternal implements MappingChainContext {
+
+    private final ResourceResolver resourceResolver;
+    private final Map<String, Object> attributes = new HashMap<>();
+    private final Map<String, SlingUri> intermediateMappings = new LinkedHashMap<>();
+    private boolean skipRemainingChain = false;
+
+
+    MappingChainContextInternal(ResourceResolver resourceResolver) {
+        this.resourceResolver = resourceResolver;
+    }
+
+    @Override
+    public void skipRemainingChain() {
+        skipRemainingChain = true;
+    }
+
+    @Override
+    public Map<String, Object> getAttributes() {
+        return attributes;
+    }
+
+    @Override
+    public Map<String, SlingUri> getIntermediateMappings() {
+        return Collections.unmodifiableMap(intermediateMappings);
+    }
+
+    @Override
+    public ResourceResolver getResourceResolver() {
+        return resourceResolver;
+    }
+
+    boolean isMarkedAsSkipRemainingChain() {
+        return skipRemainingChain;
+    }
+
+    void addIntermediateMapping(String name, SlingUri resourceUri) {
+        intermediateMappings.put(name, resourceUri);
+    }
+
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/urimapping/MappingChainResult.java b/src/main/java/org/apache/sling/resourceresolver/impl/urimapping/MappingChainResult.java
new file mode 100644
index 0000000..ad27faa
--- /dev/null
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/urimapping/MappingChainResult.java
@@ -0,0 +1,45 @@
+/*
+ * 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.urimapping;
+
+import java.util.Map;
+
+import org.apache.sling.api.resource.mapping.PathToUriMappingService;
+import org.apache.sling.api.uri.SlingUri;
+
+class MappingChainResult implements PathToUriMappingService.Result {
+
+    private final SlingUri slingUri;
+    private final Map<String, SlingUri> intermediateMappings;
+
+    public MappingChainResult(SlingUri slingUri, Map<String, SlingUri> intermediateMappings) {
+        this.slingUri = slingUri;
+        this.intermediateMappings = intermediateMappings;
+    }
+
+    public SlingUri getUri() {
+        return slingUri;
+    }
+
+    public Map<String, SlingUri> getIntermediateMappings() {
+        return intermediateMappings;
+    }
+
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/urimapping/PathToUriMappingServiceImpl.java b/src/main/java/org/apache/sling/resourceresolver/impl/urimapping/PathToUriMappingServiceImpl.java
new file mode 100644
index 0000000..c0642c7
--- /dev/null
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/urimapping/PathToUriMappingServiceImpl.java
@@ -0,0 +1,226 @@
+/*
+ * 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.urimapping;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.api.resource.mapping.PathToUriMappingService;
+import org.apache.sling.api.uri.SlingUri;
+import org.apache.sling.api.uri.SlingUriBuilder;
+import org.apache.sling.resourceresolver.impl.JcrNamespaceMangler;
+import org.apache.sling.spi.urimapping.SlingUriMapper;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicy;
+import org.osgi.service.component.annotations.ReferencePolicyOption;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Component
+public class PathToUriMappingServiceImpl implements PathToUriMappingService {
+
+    private static final Logger LOG = LoggerFactory.getLogger(PathToUriMappingServiceImpl.class);
+
+    private static final String KEY_INTIAL = "intial";
+    private static final String SUBSERVICE_NAME_MAPPING = "mapping";
+
+    @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY)
+    volatile List<SlingUriMapper> resourceMappers;
+
+    @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY)
+    volatile ResourceResolverFactory resourceResolverFactory;
+
+    JcrNamespaceMangler jcrNamespaceMangler = new JcrNamespaceMangler();
+    
+    @Activate
+    public void activate() {
+
+        if (resourceMappers != null) {
+            String resourceMappersStr = resourceMappers.stream().map(rm -> rm.getClass().toString()).collect(Collectors.joining("\n"));
+            LOG.info("Available Resource Mappers:\n{}", resourceMappersStr);
+        } else {
+            LOG.info("No Resource Mappers available");
+        }
+    }
+
+    public List<String> getAvailableResourceMappers() {
+        return resourceMappers.stream().map(rm -> rm.getClass().toString()).collect(Collectors.toList());
+    }
+
+    @NotNull
+    public MappingChainResult map(@Nullable HttpServletRequest request, @NotNull String resourcePath) {
+
+        try (ResourceResolver rr = getResourceResolver()) {
+            MappingChainContextInternal mappingContext = new MappingChainContextInternal(rr);
+            SlingUri resourceUri = SlingUriBuilder.parse(resourcePath, rr).build();
+            addIntermediateMapping(mappingContext, resourceUri, KEY_INTIAL);
+            if (resourceMappers != null) {
+                for (SlingUriMapper mapper : resourceMappers) {
+                    String mapperName = mapper.getClass().getName();
+                    LOG.trace("map(): Using {} for {}", mapperName, resourceUri);
+                    SlingUri input = resourceUri;
+                    resourceUri = mapper.map(resourceUri, request, mappingContext);
+                    if (input != resourceUri) {
+                        addIntermediateMapping(mappingContext, resourceUri, mapperName);
+                    }
+
+                    if (chancelChain(mappingContext, mapperName)) {
+                        break;
+                    }
+                }
+            }
+
+            if (LOG.isDebugEnabled()) {
+                logResult(mappingContext, "map()");
+            }
+
+            return new MappingChainResult(resourceUri, mappingContext.getIntermediateMappings());
+        }
+    }
+
+    @NotNull
+    public MappingChainResult resolve(@Nullable HttpServletRequest request, @Nullable String path) {
+
+        if (request == null) {
+            throw new IllegalArgumentException("Parameter HttpServletRequest must not be null");
+        }
+
+        try (ResourceResolver rr = getResourceResolver()) {
+            
+            MappingChainContextInternal mappingContext = new MappingChainContextInternal(rr);
+
+            SlingUri resourceUri = path != null ? SlingUriBuilder.parse(unmangleNamespaces(path, rr), rr).build()
+                    : getSlingUriFromRequest(request, rr);
+            addIntermediateMapping(mappingContext, resourceUri, KEY_INTIAL);
+            if (resourceMappers != null) {
+                for (SlingUriMapper mapper : reversedList(resourceMappers)) {
+                    String mapperName = mapper.getClass().getName();
+                    LOG.trace("resolve(): Using {} for {}", mapperName, resourceUri);
+                    SlingUri input = resourceUri;
+                    resourceUri = mapper.resolve(resourceUri, request, mappingContext);
+                    if (input != resourceUri) {
+                        addIntermediateMapping(mappingContext, resourceUri, mapperName);
+                    }
+
+                    if (chancelChain(mappingContext, mapperName)) {
+                        break;
+                    }
+                }
+            }
+
+            if (LOG.isDebugEnabled()) {
+                logResult(mappingContext, "resolve()");
+            }
+
+            return new MappingChainResult(resourceUri, mappingContext.getIntermediateMappings());
+        }
+
+    }
+
+    private String unmangleNamespaces(String path, ResourceResolver rr) {
+        return jcrNamespaceMangler.unmangleNamespaces(rr, LOG, path);
+    }
+
+
+    private boolean chancelChain(MappingChainContextInternal mappingContext, String name) {
+        if (mappingContext.isMarkedAsSkipRemainingChain()) {
+            LOG.debug("{} cancelled remaining chain", name);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    private void addIntermediateMapping(MappingChainContextInternal mappingContext, SlingUri resourceUri, String name) {
+        mappingContext.addIntermediateMapping(name, resourceUri);
+        LOG.trace("{} ajusted SlingUri: {}", name, resourceUri);
+    }
+
+    private <T> List<T> reversedList(List<T> list) {
+        List<T> reversedList = new ArrayList<>(list);
+        Collections.reverse(reversedList);
+        return reversedList;
+    }
+
+    private void logResult(MappingChainContextInternal mappingContext, String method) {
+        Map<String, SlingUri> intermediateMappings = mappingContext.getIntermediateMappings();
+        if (intermediateMappings.size() == 1) {
+            LOG.debug("\n{}:\nUNCHANGED {}", method, intermediateMappings.values().iterator().next());
+        } else {
+            LOG.debug("\n{}:\n{}", method,
+                    intermediateMappings.entrySet().stream()
+                            .map(e -> StringUtils.leftPad(e.getKey().replaceAll("([a-z])[^.]*\\.(?=.*\\..*$)", "$1."), 50) + " -> "
+                                    + e.getValue())
+                            .collect(Collectors.joining("\n")));
+        }
+    }
+
+    protected ResourceResolver getResourceResolver() {
+
+        try {
+            final Map<String, Object> authenticationInfo = new HashMap<>();
+            authenticationInfo.put(ResourceResolverFactory.SUBSERVICE, SUBSERVICE_NAME_MAPPING);
+            ResourceResolver serviceResourceResolver = resourceResolverFactory.getServiceResourceResolver(authenticationInfo);
+            return serviceResourceResolver;
+        } catch (LoginException e) {
+            throw new IllegalStateException("Cannot create ResourceResolver: " + e, e);
+        }
+    }
+
+    private SlingUri getSlingUriFromRequest(final HttpServletRequest request, ResourceResolver rr) {
+        final StringBuilder sb = new StringBuilder();
+        if (request.getServletPath() != null) {
+            sb.append(request.getServletPath());
+        }
+        if (request.getPathInfo() != null) {
+            sb.append(request.getPathInfo());
+        }
+        String path = sb.toString();
+        // Get the path used to select the authenticator, if the SlingServlet
+        // itself has been requested without any more info, this will be empty
+        // and we assume the root (SLING-722)
+        if (path.length() == 0) {
+            path = "/";
+        }
+
+        return SlingUriBuilder.create()
+                .setResourceResolver(rr)
+                .setScheme(request.getScheme())
+                .setHost(request.getServerName())
+                .setPort(request.getServerPort())
+                .setPath(unmangleNamespaces(path, rr))
+                .setQuery(request.getQueryString())
+                .build();
+    }
+}
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/urimapping/defaultmappers/BasePathMapper.java b/src/main/java/org/apache/sling/resourceresolver/impl/urimapping/defaultmappers/BasePathMapper.java
new file mode 100644
index 0000000..23bf26b
--- /dev/null
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/urimapping/defaultmappers/BasePathMapper.java
@@ -0,0 +1,84 @@
+/*
+ * 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.urimapping.defaultmappers;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.api.uri.SlingUri;
+import org.apache.sling.spi.urimapping.MappingChainContext;
+import org.apache.sling.spi.urimapping.SlingUriMapper;
+import org.jetbrains.annotations.NotNull;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.ConfigurationPolicy;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Component(configurationPolicy = ConfigurationPolicy.REQUIRE)
+@Designate(ocd = BasePathMapper.Config.class, factory = true)
+public class BasePathMapper implements SlingUriMapper {
+    private static final Logger LOG = LoggerFactory.getLogger(BasePathMapper.class);
+
+    @ObjectClassDefinition(name = "Apache Sling Resource Mapper: Base Path", description = "Maps a base path in both ways")
+    public @interface Config {
+        @AttributeDefinition(name = "Content Base Path", description = "Content Base Path to be remove when mapping and to be added when resolving.")
+        String basePath();
+    }
+
+    private String basePath;
+
+    @Activate
+    public void activate(Config config) {
+        basePath = config.basePath();
+        LOG.info("Automatic addition of html extension active for paths {}", basePath);
+    }
+
+    @Override
+    public SlingUri resolve(@NotNull SlingUri slingUri, HttpServletRequest request, MappingChainContext context) {
+
+        if (StringUtils.equals(slingUri.getResourcePath(), "/") || StringUtils.equals(slingUri.getResourcePath(), "")) {
+            return slingUri;
+        }
+        String expandedPath = basePath + slingUri.getResourcePath();
+        Resource resource = context.getResourceResolver().resolve(expandedPath);
+        if (!ResourceUtil.isSyntheticResource(resource)) {
+            return slingUri.adjust(b -> b.setResourcePath(expandedPath));
+        } else {
+            return slingUri;
+        }
+    }
+
+    @Override
+    public SlingUri map(@NotNull SlingUri slingUri, HttpServletRequest request, MappingChainContext context) {
+
+        if (StringUtils.startsWith(slingUri.getResourcePath(), basePath)) {
+            return slingUri.adjust(b -> b.setResourcePath(StringUtils.substringAfter(slingUri.getResourcePath(), basePath)));
+        } else {
+            return slingUri;
+        }
+
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/urimapping/defaultmappers/EnsureHtmlExtensionMapper.java b/src/main/java/org/apache/sling/resourceresolver/impl/urimapping/defaultmappers/EnsureHtmlExtensionMapper.java
new file mode 100644
index 0000000..d11eb29
--- /dev/null
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/urimapping/defaultmappers/EnsureHtmlExtensionMapper.java
@@ -0,0 +1,80 @@
+/*
+ * 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.urimapping.defaultmappers;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.sling.api.uri.SlingUri;
+import org.apache.sling.spi.urimapping.MappingChainContext;
+import org.apache.sling.spi.urimapping.SlingUriMapper;
+import org.jetbrains.annotations.NotNull;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.ConfigurationPolicy;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Component(configurationPolicy = ConfigurationPolicy.REQUIRE)
+@Designate(ocd = EnsureHtmlExtensionMapper.Config.class)
+public class EnsureHtmlExtensionMapper implements SlingUriMapper {
+    private static final Logger LOG = LoggerFactory.getLogger(EnsureHtmlExtensionMapper.class);
+
+    @ObjectClassDefinition(name = "Apache Sling Resource Mapper: Ensure HTML Extension", description = "Ensures html extension in links without extension")
+    public @interface Config {
+
+        @AttributeDefinition(name = "Include Regex", description = "Regex to restrict paths where the extension should automatically be added")
+        String includeRegex();
+    }
+
+    private String includeRegex;
+
+    @Activate
+    public void activate(Config config) {
+        includeRegex = config.includeRegex();
+        LOG.info("Automatic addition of html extension active for paths {}", StringUtils.defaultIfBlank(includeRegex, "<all>"));
+    }
+
+    private boolean matchesRegex(String path) {
+        return StringUtils.isBlank(includeRegex) || path.matches(includeRegex);
+    }
+
+    @Override
+    public SlingUri resolve(@NotNull SlingUri slingUri, HttpServletRequest request, MappingChainContext context) {
+        return slingUri;
+    }
+
+    @Override
+    public SlingUri map(@NotNull SlingUri slingUri, HttpServletRequest request, MappingChainContext context) {
+        if (StringUtils.isNotBlank(slingUri.getResourcePath())
+                && !StringUtils.contains(slingUri.getResourcePath(), ".")
+                && StringUtils.isBlank(slingUri.getExtension())
+                && matchesRegex(slingUri.getResourcePath())) {
+            LOG.debug("Adding extension to URL {} with path={} and ext={}", slingUri, slingUri.getResourcePath(),
+                    slingUri.getExtension());
+            return slingUri.adjust(b -> b.setExtension("html"));
+        } else {
+            return slingUri;
+        }
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/resourceresolver/impl/EtcMappingResourceResolverTest.java b/src/test/java/org/apache/sling/resourceresolver/impl/EtcMappingResourceResolverTest.java
index ca2d6a6..380be0a 100644
--- a/src/test/java/org/apache/sling/resourceresolver/impl/EtcMappingResourceResolverTest.java
+++ b/src/test/java/org/apache/sling/resourceresolver/impl/EtcMappingResourceResolverTest.java
@@ -16,40 +16,10 @@
  */
 package org.apache.sling.resourceresolver.impl;
 
-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.observation.ResourceChange;
-import org.apache.sling.api.resource.path.Path;
-import org.apache.sling.resourceresolver.impl.mapping.MapConfigurationProvider;
-import org.apache.sling.resourceresolver.impl.mapping.MapEntries;
-import org.apache.sling.resourceresolver.impl.mapping.StringInterpolationProviderConfiguration;
-import org.apache.sling.resourceresolver.impl.mapping.StringInterpolationProvider;
-import org.apache.sling.resourceresolver.impl.mapping.StringInterpolationProviderImpl;
-import org.apache.sling.resourceresolver.impl.providers.ResourceProviderHandler;
-import org.apache.sling.resourceresolver.impl.providers.ResourceProviderStorage;
-import org.apache.sling.resourceresolver.impl.providers.ResourceProviderTracker;
-import org.apache.sling.serviceusermapping.ServiceUserMapper;
-import org.apache.sling.spi.resource.provider.ResourceProvider;
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.osgi.framework.Bundle;
-import org.osgi.framework.BundleContext;
-import org.osgi.service.event.EventAdmin;
-
-import javax.servlet.http.HttpServletRequest;
-import java.io.File;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
 import static java.util.Arrays.asList;
 import static org.apache.sling.resourceresolver.impl.MockedResourceResolverImplTest.createRPHandler;
 import static org.apache.sling.resourceresolver.impl.ResourceResolverImpl.PROP_REDIRECT_INTERNAL;
 import static org.apache.sling.resourceresolver.impl.mapping.MapEntries.PROP_REDIRECT_EXTERNAL;
-import static org.apache.sling.resourceresolver.util.MockTestUtil.ExpectedEtcMapping;
 import static org.apache.sling.resourceresolver.util.MockTestUtil.buildResource;
 import static org.apache.sling.resourceresolver.util.MockTestUtil.callInaccessibleMethod;
 import static org.apache.sling.resourceresolver.util.MockTestUtil.checkInternalResource;
@@ -64,6 +34,38 @@
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
 
+import java.io.File;
+import java.util.ArrayList;
+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.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.api.resource.observation.ResourceChange;
+import org.apache.sling.api.resource.path.Path;
+import org.apache.sling.resourceresolver.impl.mapping.MapConfigurationProvider;
+import org.apache.sling.resourceresolver.impl.mapping.MapEntries;
+import org.apache.sling.resourceresolver.impl.mapping.StringInterpolationProvider;
+import org.apache.sling.resourceresolver.impl.mapping.StringInterpolationProviderConfiguration;
+import org.apache.sling.resourceresolver.impl.mapping.StringInterpolationProviderImpl;
+import org.apache.sling.resourceresolver.impl.providers.ResourceProviderHandler;
+import org.apache.sling.resourceresolver.impl.providers.ResourceProviderStorage;
+import org.apache.sling.resourceresolver.impl.providers.ResourceProviderTracker;
+import org.apache.sling.resourceresolver.util.MockTestUtil;
+import org.apache.sling.resourceresolver.util.MockTestUtil.ExpectedEtcMapping;
+import org.apache.sling.serviceusermapping.ServiceUserMapper;
+import org.apache.sling.spi.resource.provider.ResourceProvider;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.event.EventAdmin;
+
 /**
  * These are the same tests as in the EtcMappingMapEntriesTest but in this
  * class we are actually mocking the Resource Resolver Factory and its classes
@@ -130,6 +132,8 @@
         setInaccessibleField("observationPaths", activator, new Path[] {new Path("/")});
         ServiceUserMapper serviceUserMapper = mock(ServiceUserMapper.class);
         setInaccessibleField("serviceUserMapper", activator, serviceUserMapper);
+        setInaccessibleField("pathToUriMappingService", activator, MockTestUtil.createPathToUriMappingServiceMock(resourceResolver));
+
         commonFactory = spy(new CommonResourceResolverFactoryImpl(activator));
         when(bundleContext.getBundle()).thenReturn(bundle);
         when(bundleContext.getDataFile("vanityBloomFilter.txt")).thenReturn(vanityBloomFilterFile);
diff --git a/src/test/java/org/apache/sling/resourceresolver/impl/MockedResourceResolverImplTest.java b/src/test/java/org/apache/sling/resourceresolver/impl/MockedResourceResolverImplTest.java
index b960a82..b853473 100644
--- a/src/test/java/org/apache/sling/resourceresolver/impl/MockedResourceResolverImplTest.java
+++ b/src/test/java/org/apache/sling/resourceresolver/impl/MockedResourceResolverImplTest.java
@@ -42,6 +42,7 @@
 import org.apache.sling.api.resource.ResourceResolver;
 import org.apache.sling.api.resource.ResourceResolverFactory;
 import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.api.resource.mapping.PathToUriMappingService;
 import org.apache.sling.api.wrappers.ValueMapDecorator;
 import org.apache.sling.resourceresolver.impl.mapping.MapEntries;
 import org.apache.sling.resourceresolver.impl.observation.ResourceChangeListenerWhiteboard;
@@ -49,6 +50,7 @@
 import org.apache.sling.resourceresolver.impl.providers.ResourceProviderInfo;
 import org.apache.sling.resourceresolver.impl.providers.ResourceProviderStorage;
 import org.apache.sling.resourceresolver.impl.providers.ResourceProviderTracker;
+import org.apache.sling.resourceresolver.util.MockTestUtil;
 import org.apache.sling.serviceusermapping.ServiceUserMapper;
 import org.apache.sling.spi.resource.provider.QueryLanguageProvider;
 import org.apache.sling.spi.resource.provider.ResolveContext;
@@ -141,7 +143,12 @@
     @SuppressWarnings("unchecked")
     @Before
     public void before() throws LoginException {
-        activator = new ResourceResolverFactoryActivator();
+        activator = new ResourceResolverFactoryActivator() {
+            @Override
+            public PathToUriMappingService getPathToUriMappingService() {
+                return MockTestUtil.createPathToUriMappingServiceMock(null);
+            }
+        };
 
         // system bundle access
         final Bundle systemBundle = Mockito.mock(Bundle.class);
diff --git a/src/test/java/org/apache/sling/resourceresolver/impl/ProviderHandlerTest.java b/src/test/java/org/apache/sling/resourceresolver/impl/ProviderHandlerTest.java
index d9592c4..20b3d17 100644
--- a/src/test/java/org/apache/sling/resourceresolver/impl/ProviderHandlerTest.java
+++ b/src/test/java/org/apache/sling/resourceresolver/impl/ProviderHandlerTest.java
@@ -34,6 +34,7 @@
 import org.apache.sling.resourceresolver.impl.providers.ResourceProviderHandler;
 import org.apache.sling.resourceresolver.impl.providers.ResourceProviderStorage;
 import org.apache.sling.resourceresolver.impl.providers.ResourceProviderStorageProvider;
+import org.apache.sling.resourceresolver.util.MockTestUtil;
 import org.apache.sling.spi.resource.provider.ResolveContext;
 import org.apache.sling.spi.resource.provider.ResourceContext;
 import org.apache.sling.spi.resource.provider.ResourceProvider;
@@ -67,6 +68,7 @@
                     return new ResourceProviderStorage(Arrays.asList(h));
                 }
             });
+        activator.pathToUriMappingService = MockTestUtil.createPathToUriMappingServiceMock(resolver);
 
         final Resource parent = resolver.getResource(ResourceUtil.getParent(servletpath));
         assertNotNull("Parent must be available", parent);
diff --git a/src/test/java/org/apache/sling/resourceresolver/impl/ResourceDecoratorTestBase.java b/src/test/java/org/apache/sling/resourceresolver/impl/ResourceDecoratorTestBase.java
index 8213179..6795875 100644
--- a/src/test/java/org/apache/sling/resourceresolver/impl/ResourceDecoratorTestBase.java
+++ b/src/test/java/org/apache/sling/resourceresolver/impl/ResourceDecoratorTestBase.java
@@ -36,10 +36,12 @@
 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.api.resource.mapping.PathToUriMappingService;
 import org.apache.sling.resourceresolver.impl.helper.ResourceDecoratorTracker;
 import org.apache.sling.resourceresolver.impl.providers.ResourceProviderHandler;
 import org.apache.sling.resourceresolver.impl.providers.ResourceProviderStorage;
 import org.apache.sling.resourceresolver.impl.providers.ResourceProviderStorageProvider;
+import org.apache.sling.resourceresolver.util.MockTestUtil;
 import org.apache.sling.spi.resource.provider.QueryLanguageProvider;
 import org.apache.sling.spi.resource.provider.ResolveContext;
 import org.apache.sling.spi.resource.provider.ResourceContext;
@@ -141,6 +143,11 @@
             public ResourceAccessSecurityTracker getResourceAccessSecurityTracker() {
                 return new ResourceAccessSecurityTracker();
             }
+
+            @Override
+            public PathToUriMappingService getPathToUriMappingService() {
+                return MockTestUtil.createPathToUriMappingServiceMock(null);
+            }
         };
 
         final List<ResourceProviderHandler> list = Arrays.asList(MockedResourceResolverImplTest.createRPHandler(provider, "A-provider", 0L, "/"));
diff --git a/src/test/java/org/apache/sling/resourceresolver/impl/ResourceResolverImplTest.java b/src/test/java/org/apache/sling/resourceresolver/impl/ResourceResolverImplTest.java
index 3eee2cf..fde657b 100644
--- a/src/test/java/org/apache/sling/resourceresolver/impl/ResourceResolverImplTest.java
+++ b/src/test/java/org/apache/sling/resourceresolver/impl/ResourceResolverImplTest.java
@@ -52,6 +52,7 @@
 import org.apache.sling.resourceresolver.impl.providers.ResourceProviderHandler;
 import org.apache.sling.resourceresolver.impl.providers.ResourceProviderStorage;
 import org.apache.sling.resourceresolver.impl.providers.ResourceProviderTracker;
+import org.apache.sling.resourceresolver.util.MockTestUtil;
 import org.apache.sling.spi.resource.provider.ResolveContext;
 import org.apache.sling.spi.resource.provider.ResourceContext;
 import org.apache.sling.spi.resource.provider.ResourceProvider;
@@ -91,6 +92,7 @@
         ResourceResolverFactoryActivator activator = new ResourceResolverFactoryActivator();
         activator.resourceProviderTracker = resourceProviderTracker;
         activator.resourceAccessSecurityTracker = new ResourceAccessSecurityTracker();
+        activator.pathToUriMappingService = MockTestUtil.createPathToUriMappingServiceMock(null);
         commonFactory = new CommonResourceResolverFactoryImpl(activator);
         final Bundle usingBundle = mock(Bundle.class);
         resFac = new ResourceResolverFactoryImpl(commonFactory, usingBundle, null);
diff --git a/src/test/java/org/apache/sling/resourceresolver/impl/ResourceResolverMangleNamespacesTest.java b/src/test/java/org/apache/sling/resourceresolver/impl/ResourceResolverMangleNamespacesTest.java
index 8877882..dabc021 100644
--- a/src/test/java/org/apache/sling/resourceresolver/impl/ResourceResolverMangleNamespacesTest.java
+++ b/src/test/java/org/apache/sling/resourceresolver/impl/ResourceResolverMangleNamespacesTest.java
@@ -23,19 +23,21 @@
 import java.util.Arrays;
 import java.util.Iterator;
 
-import org.jetbrains.annotations.Nullable;
-import org.jetbrains.annotations.NotNull;
 import javax.jcr.RepositoryException;
 import javax.jcr.Session;
 
 import org.apache.commons.collections4.IteratorUtils;
 import org.apache.sling.api.resource.LoginException;
 import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.mapping.PathToUriMappingService;
 import org.apache.sling.resourceresolver.impl.providers.ResourceProviderStorage;
 import org.apache.sling.resourceresolver.impl.providers.ResourceProviderStorageProvider;
+import org.apache.sling.resourceresolver.util.MockTestUtil;
 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.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.Mock;
@@ -65,6 +67,11 @@
             public boolean isMangleNamespacePrefixes() {
                 return true;
             }
+
+            @Override
+            public PathToUriMappingService getPathToUriMappingService() {
+                return MockTestUtil.createPathToUriMappingServiceMock(rr);
+            }
         };
 
         Mockito.when(mockedSession.getNamespacePrefix(NS_PREFIX)).thenReturn(NS_URL);
diff --git a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/AbstractMappingMapEntriesTest.java b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/AbstractMappingMapEntriesTest.java
index 44e04a9..cf54a3b 100644
--- a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/AbstractMappingMapEntriesTest.java
+++ b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/AbstractMappingMapEntriesTest.java
@@ -16,22 +16,17 @@
  */
 package org.apache.sling.resourceresolver.impl.mapping;
 
-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.ValueMap;
-import org.apache.sling.api.resource.path.Path;
-import org.apache.sling.api.wrappers.ValueMapDecorator;
-import org.junit.After;
-import org.junit.Before;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
-import org.osgi.framework.Bundle;
-import org.osgi.framework.BundleContext;
-import org.osgi.service.event.EventAdmin;
+import static org.apache.sling.resourceresolver.util.MockTestUtil.createStringInterpolationProviderConfiguration;
+import static org.apache.sling.resourceresolver.util.MockTestUtil.setupStringInterpolationProvider;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyMap;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.withSettings;
 
 import java.io.File;
 import java.lang.reflect.Field;
@@ -46,17 +41,23 @@
 import java.util.concurrent.Future;
 import java.util.concurrent.Semaphore;
 
-import static org.apache.sling.resourceresolver.util.MockTestUtil.createStringInterpolationProviderConfiguration;
-import static org.apache.sling.resourceresolver.util.MockTestUtil.setupStringInterpolationProvider;
-import static org.junit.Assert.fail;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyMap;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-import static org.mockito.Mockito.withSettings;
+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.ValueMap;
+import org.apache.sling.api.resource.path.Path;
+import org.apache.sling.api.wrappers.ValueMapDecorator;
+import org.apache.sling.resourceresolver.impl.ResourceResolverFactoryImpl;
+import org.junit.After;
+import org.junit.Before;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.event.EventAdmin;
 
 /**
  * These are tests that are testing the Sling Interpolation Feature (SLING-7768)
diff --git a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/EtcMappingMapEntriesTest.java b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/EtcMappingMapEntriesTest.java
index a98ca6e..1f3fa13 100644
--- a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/EtcMappingMapEntriesTest.java
+++ b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/EtcMappingMapEntriesTest.java
@@ -16,6 +16,23 @@
  */
 package org.apache.sling.resourceresolver.impl.mapping;
 
+import static java.util.Arrays.asList;
+import static org.apache.sling.resourceresolver.impl.MockedResourceResolverImplTest.createRPHandler;
+import static org.apache.sling.resourceresolver.impl.ResourceResolverImpl.PROP_REDIRECT_INTERNAL;
+import static org.apache.sling.resourceresolver.impl.mapping.MapEntries.PROP_REDIRECT_EXTERNAL;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import java.lang.reflect.Method;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+
 import org.apache.sling.api.resource.Resource;
 import org.apache.sling.api.resource.ResourceResolver;
 import org.apache.sling.api.resource.path.Path;
@@ -26,6 +43,8 @@
 import org.apache.sling.resourceresolver.impl.providers.ResourceProviderHandler;
 import org.apache.sling.resourceresolver.impl.providers.ResourceProviderStorage;
 import org.apache.sling.resourceresolver.impl.providers.ResourceProviderTracker;
+import org.apache.sling.resourceresolver.util.MockTestUtil;
+import org.apache.sling.resourceresolver.util.MockTestUtil.ExpectedEtcMapping;
 import org.apache.sling.serviceusermapping.ServiceUserMapper;
 import org.apache.sling.spi.resource.provider.ResolveContext;
 import org.apache.sling.spi.resource.provider.ResourceContext;
@@ -34,23 +53,6 @@
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
 
-import javax.servlet.http.HttpServletRequest;
-import java.lang.reflect.Method;
-import java.util.Iterator;
-import java.util.List;
-
-import static java.util.Arrays.asList;
-import static org.apache.sling.resourceresolver.impl.MockedResourceResolverImplTest.createRPHandler;
-import static org.apache.sling.resourceresolver.impl.ResourceResolverImpl.PROP_REDIRECT_INTERNAL;
-import static org.apache.sling.resourceresolver.impl.mapping.MapEntries.PROP_REDIRECT_EXTERNAL;
-import static org.apache.sling.resourceresolver.util.MockTestUtil.ExpectedEtcMapping;
-import static org.junit.Assert.assertEquals;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.when;
-
 /**
  * These tests are for the /etc/map setup of the Map Entries when
  * an /etc/map is present.
@@ -191,7 +193,7 @@
         final Bundle usingBundle = mock(Bundle.class);
         ResourceResolverFactoryImpl resFac = new ResourceResolverFactoryImpl(commonFactory, usingBundle, null);
         ResourceResolver resResolver = resFac.getAdministrativeResourceResolver(null);
-
+        when(activator.getPathToUriMappingService()).thenReturn(MockTestUtil.createPathToUriMappingServiceMock(resResolver));
         HttpServletRequest request = mock(HttpServletRequest.class);
         when(request.getScheme()).thenReturn("http");
         when(request.getServerName()).thenReturn("localhost");
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
index 0e2cc54..fa71d15 100644
--- a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/ResourceMapperImplTest.java
+++ b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/ResourceMapperImplTest.java
@@ -39,9 +39,11 @@
 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.PathToUriMappingService;
 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.resourceresolver.util.MockTestUtil;
 import org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl;
 import org.apache.sling.spi.resource.provider.ResourceProvider;
 import org.apache.sling.testing.mock.osgi.junit.OsgiContext;
@@ -97,6 +99,7 @@
         ctx.registerInjectActivateService(new ServiceUserMapperImpl());
         ctx.registerInjectActivateService(new ResourceAccessSecurityTracker());
         ctx.registerInjectActivateService(new StringInterpolationProviderImpl());
+        ctx.registerService(PathToUriMappingService.class, MockTestUtil.createPathToUriMappingServiceMock(resolver));
 
         InMemoryResourceProvider resourceProvider = new InMemoryResourceProvider();
         resourceProvider.putResource("/"); // root
diff --git a/src/test/java/org/apache/sling/resourceresolver/util/MockTestUtil.java b/src/test/java/org/apache/sling/resourceresolver/util/MockTestUtil.java
index 5291b5b..f8e696f 100644
--- a/src/test/java/org/apache/sling/resourceresolver/util/MockTestUtil.java
+++ b/src/test/java/org/apache/sling/resourceresolver/util/MockTestUtil.java
@@ -16,12 +16,30 @@
  */
 package org.apache.sling.resourceresolver.util;
 
-import junit.framework.TestCase;
+import static java.util.Arrays.asList;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.withSettings;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+
 import org.apache.sling.api.resource.NonExistingResource;
 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.ValueMap;
+import org.apache.sling.api.resource.mapping.PathToUriMappingService;
 import org.apache.sling.api.wrappers.ValueMapDecorator;
 import org.apache.sling.resourceresolver.impl.SimpleValueMapImpl;
 import org.apache.sling.resourceresolver.impl.helper.RedirectResource;
@@ -36,22 +54,7 @@
 import org.mockito.stubbing.Answer;
 import org.osgi.framework.BundleContext;
 
-import javax.servlet.http.HttpServletRequest;
-import java.lang.reflect.Field;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Iterator;
-import java.util.List;
-
-import static java.util.Arrays.asList;
-import static org.hamcrest.Matchers.instanceOf;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-import static org.mockito.Mockito.withSettings;
+import junit.framework.TestCase;
 
 public class MockTestUtil {
 
@@ -424,4 +427,9 @@
             assertEquals("Wrong Redirect Type (ext/int) for: " + title, this.internal, mapEntry.isInternal());
         }
     }
+
+    public static PathToUriMappingService createPathToUriMappingServiceMock(final ResourceResolver rr) {
+        return new TestPathToUriMappingService();
+    }
+
 }
diff --git a/src/test/java/org/apache/sling/resourceresolver/util/TestPathToUriMappingService.java b/src/test/java/org/apache/sling/resourceresolver/util/TestPathToUriMappingService.java
new file mode 100644
index 0000000..93ce2ec
--- /dev/null
+++ b/src/test/java/org/apache/sling/resourceresolver/util/TestPathToUriMappingService.java
@@ -0,0 +1,59 @@
+/*
+ * 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.util;
+
+import java.util.Collections;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.sling.api.resource.mapping.PathToUriMappingService;
+import org.apache.sling.api.uri.SlingUri;
+import org.apache.sling.api.uri.SlingUriBuilder;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+class TestPathToUriMappingService implements PathToUriMappingService {
+
+    static class MappingResult implements PathToUriMappingService.Result {
+        private final @NotNull SlingUri slingUri;
+
+        MappingResult(@NotNull SlingUri slingUri) {
+            this.slingUri = slingUri;
+        }
+
+        @Override
+        public @NotNull SlingUri getUri() {
+            return slingUri;
+        }
+
+        @Override
+        public @NotNull Map<String, SlingUri> getIntermediateMappings() {
+            return Collections.emptyMap();
+        }
+    }
+
+    @Override
+    public Result resolve(@Nullable HttpServletRequest request, @NotNull String path) {
+        return new MappingResult(SlingUriBuilder.parse(path, null).build());
+    }
+
+    @Override
+    public Result map(@Nullable HttpServletRequest request, @NotNull String path) {
+        return new MappingResult(SlingUriBuilder.parse(path, null).build());
+    }
+}
\ No newline at end of file