Merge branch 'master' into feature/SLING-7768
diff --git a/README.md b/README.md
index 02bf7bc..00d8c1c 100644
--- a/README.md
+++ b/README.md
@@ -7,3 +7,57 @@
 This module is part of the [Apache Sling](https://sling.apache.org) project.
 
 This bundle provides the Resource Resolver and Resource Resolver Factory
+
+## ETC Map String Interpolation
+
+Setting up ETC Mappings (/etc/map) for different instances like dev, stage,
+qa and production was time consuming and error prone due to copy-n-paste
+errors. 
+As a new feature Sling now supports String Interpolation in the /etc/map.
+With it it is possible to create a single set of etc-mapping and then adjust
+the actual values of an instance by an OSGi configuration.
+By default a variable name is enclosed in **${}** with a **$** as escape
+character and no in-variable-substitution. All of that is configurable
+together with the actual value map.
+
+### Setup
+
+The Substitution Configuration can be found in the OSGi Configuration
+as **Apache Sling String Interpolation Provider**. The property **Placeholder
+Values** takes a list of **key=value** entries where each of them map a
+variable with its actual value.
+In our little introduction we add an entry of
+**phv.default.host.name=localhost**. Save the configuration for now.
+Before going on make sure that you know Mapping Location configuration
+in the OSGi configuration of **Apache Sling Resource Resolver Factory**.
+Now to to **composum** and go to that node. If it does not exist then create
+one. The mapping should look like this:
+* etc
+    * map
+        * http
+            * ${phv.fq.host.name}.8080
+            
+Opening the page **http://localhost:8080/starter/index.html** should
+work just fine.
+
+### Testing
+
+Now got back to the String Interpolation configuration and change the value
+to **qa.author.acme.com** and save it.
+
+For local testing open your **hosts** file (/etc/hosts on Unix) and add a
+line like this:
+```
+127.0.0.1 qa.author.acme.com
+```
+save it and test with `ping qa.author.acme.com` to make sure the name
+resolves.
+Now you should be able to open the same page with:
+**http://qa.author.acme.com/starter/index.html**.
+
+Now do the same with **phv.fq.host.name=staging.author.acme.com**.
+
+The String Interpolation works with any part of the etc-map tree.
+ 
+ 
+
diff --git a/pom.xml b/pom.xml
index 970114c..744001f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -63,6 +63,32 @@
                     </execution>
                 </executions>
             </plugin>
+<!--
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <extensions>true</extensions>
+                <configuration>
+                    <instructions>
+                        <Import-Package>
+                            javax.jcr;resolution:=optional,
+                            *
+                        </Import-Package>
+                        <!- - Check if that is still needed - ->
+                        <Export-Package>
+                            org.apache.sling.resourceresolver.impl.mapping
+                        </Export-Package>
+                        <Provide-Capability>
+                            osgi.service;objectClass=javax.servlet.Servlet,
+                            osgi.service;objectClass=org.apache.sling.api.resource.ResourceResolverFactory,
+                            osgi.service;objectClass=org.apache.sling.api.resource.observation.ResourceChangeListener,
+                            osgi.service;objectClass=org.apache.sling.api.resource.runtime.RuntimeService,
+                            osgi.service;objectClass=org.apache.sling.spi.resource.provider.ResourceProvider
+                        </Provide-Capability>
+                    </instructions>
+                </configuration>
+            </plugin>
+-->
             <plugin>
                 <groupId>biz.aQute.bnd</groupId>
                 <artifactId>bnd-maven-plugin</artifactId>
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 23d7743..b925e65 100644
--- a/src/main/java/org/apache/sling/resourceresolver/impl/CommonResourceResolverFactoryImpl.java
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/CommonResourceResolverFactoryImpl.java
@@ -322,7 +322,7 @@
         }
         // set up the map entries from configuration
         try {
-            mapEntries = new MapEntries(this, bundleContext, this.activator.getEventAdmin());
+            mapEntries = new MapEntries(this, bundleContext, this.activator.getEventAdmin(), this.activator.getStringInterpolationProvider());
         } catch (final Exception e) {
             logger.error("activate: Cannot access repository, failed setting up Mapping Support", e);
         }
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 ffbcb2d..35196a2 100644
--- a/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverFactoryActivator.java
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverFactoryActivator.java
@@ -38,6 +38,7 @@
 import org.apache.sling.api.resource.runtime.RuntimeService;
 import org.apache.sling.resourceresolver.impl.helper.ResourceDecoratorTracker;
 import org.apache.sling.resourceresolver.impl.mapping.Mapping;
+import org.apache.sling.resourceresolver.impl.mapping.StringInterpolationProvider;
 import org.apache.sling.resourceresolver.impl.observation.ResourceChangeListenerWhiteboard;
 import org.apache.sling.resourceresolver.impl.providers.ResourceProviderTracker;
 import org.apache.sling.resourceresolver.impl.providers.ResourceProviderTracker.ChangeListener;
@@ -107,6 +108,10 @@
     @Reference
     EventAdmin eventAdmin;
 
+    /** Event admin. */
+    @Reference
+    StringInterpolationProvider stringInterpolationProvider;
+
     /** Service User Mapper */
     @Reference
     ServiceUserMapper serviceUserMapper;
@@ -152,6 +157,10 @@
         return this.eventAdmin;
     }
 
+    public StringInterpolationProvider getStringInterpolationProvider() {
+        return stringInterpolationProvider;
+    }
+
     /**
      * This method is called from {@link MapEntries}
      */
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntries.java b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntries.java
index 9779b3b..fa4eaf8 100644
--- a/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntries.java
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntries.java
@@ -107,6 +107,8 @@
 
     private static final String JCR_SYSTEM_PREFIX = "/jcr:system/";
 
+    static final String ALIAS_QUERY_DEFAULT = "SELECT sling:alias FROM nt:base WHERE sling:alias IS NOT NULL";
+
     static final String ANY_SCHEME_HOST = "[^/]+/[^/]+";
 
     /** default log */
@@ -140,8 +142,10 @@
 
     private boolean updateBloomFilterFile = false;
 
+    private StringInterpolationProvider stringInterpolationProvider;
+
     @SuppressWarnings({ "unchecked" })
-    public MapEntries(final MapConfigurationProvider factory, final BundleContext bundleContext, final EventAdmin eventAdmin)
+    public MapEntries(final MapConfigurationProvider factory, final BundleContext bundleContext, final EventAdmin eventAdmin, final StringInterpolationProvider stringInterpolationProvider)
         throws LoginException, IOException {
 
     	this.resolver = factory.getServiceResourceResolver(factory.getServiceUserAuthenticationInfo("mapping"));
@@ -152,6 +156,7 @@
         this.mapMaps = Collections.<MapEntry> emptyList();
         this.vanityTargets = Collections.<String,List <String>>emptyMap();
         this.aliasMap = Collections.<String, Map<String, String>>emptyMap();
+        this.stringInterpolationProvider = stringInterpolationProvider;
 
         doInit();
 
@@ -343,7 +348,6 @@
      * Remove all aliases for the content path
      * @param contentPath The content path
      * @param path Optional sub path of the vanity path
-     * @param refreshed Flag if session needs refresh
      * @return {@code true} if a change happened
      */
     private boolean removeAlias(final String contentPath, final String path, final AtomicBoolean resolverRefreshed) {
@@ -701,7 +705,7 @@
 
     /**
      * Handles the change to any of the node properties relevant for vanity URL
-     * mappings. The {@link #MapEntries(ResourceResolverFactoryImpl, BundleContext, EventAdmin)}
+     * mappings. The {@link #MapEntries(MapConfigurationProvider, BundleContext, EventAdmin, StringInterpolationProvider)}
      * constructor makes sure the event listener is registered to only get
      * appropriate events.
      */
@@ -955,6 +959,8 @@
                 name = child.getName().concat("/");
                 trailingSlash = true;
             }
+            // Check for placeholders and replace if needed
+            name = stringInterpolationProvider.substitute(name);
 
             final String childPath = parentPath.concat(name);
 
@@ -1026,16 +1032,16 @@
      */
     private Map<String, Map<String, String>> loadAliases(final ResourceResolver resolver) {
         final Map<String, Map<String, String>> map = new ConcurrentHashMap<>();
-        final String queryString = "SELECT sling:alias FROM nt:base WHERE sling:alias IS NOT NULL";
-        final Iterator<Resource> i = resolver.findResources(queryString, "sql");
-        while (i.hasNext()) {
-            final Resource resource = i.next();
-            loadAlias(resource, map);
-        }
+        final String queryString = ALIAS_QUERY_DEFAULT;
+		        final Iterator<Resource> i = resolver.findResources(queryString, "sql");
+		        while (i.hasNext()) {
+		            final Resource resource = i.next();
+		            loadAlias(resource, map);
+		        }
         return map;
     }
 
-    /**
+	/**
      * Load alias given a resource
      */
     private boolean loadAlias(final Resource resource, Map<String, Map<String, String>> map) {
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/mapping/StringInterpolationProvider.java b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/StringInterpolationProvider.java
new file mode 100644
index 0000000..12db6f5
--- /dev/null
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/StringInterpolationProvider.java
@@ -0,0 +1,39 @@
+/*
+ * 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;
+
+/**
+ * This class provides placeholders for Sling configuration settings
+ * that depend on the environment like host names / ports for dev, test,
+ * qa, staging, prod systems
+ *
+ * Placeholders are enclosed in Starting and Ending Delimiters (see PLACEHOLDER_START/END_TOKEN)
+ * The name of the placeholder can contain any character except opening or closing
+ * brackets (no nesting).
+ */
+public interface StringInterpolationProvider {
+
+    /**
+     * Replaces any placeholders with the replacement value
+     *
+     * @param text Text to be substituted
+     * @return Substituted string
+     */
+    String substitute(String text);
+}
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/mapping/StringInterpolationProviderConfiguration.java b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/StringInterpolationProviderConfiguration.java
new file mode 100644
index 0000000..7cfebd1
--- /dev/null
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/StringInterpolationProviderConfiguration.java
@@ -0,0 +1,58 @@
+/*
+ * 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 org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+
+@ObjectClassDefinition(name = "Apache Sling String Interpolation Provider",
+    description = "Configures the String Interpolation Provider and the location of its key/value pairs")
+public @interface StringInterpolationProviderConfiguration {
+
+    String DEFAULT_PREFIX = "${";
+    String DEFAULT_SUFFIX = "}";
+    char DEFAULT_ESCAPE_CHARACTER = '$';
+    boolean DEFAULT_IN_VARIABLE_SUBSTITUTION = false;
+
+    // Setup for the String Substitution
+    @AttributeDefinition(
+        name = "Substitution Prefix",
+        description = "The Prefix of the Variable to be replaced (default = '${')")
+    String substitutionPrefix() default DEFAULT_PREFIX;
+    @AttributeDefinition(
+        name = "Substitution Suffix",
+        description = "The Suffix of the Variable to be replaced (deault = '}'")
+    String substitutionSuffix() default DEFAULT_SUFFIX;
+    @AttributeDefinition(
+        name = "Substitution Escape Character",
+        description = "The Escape Character for Prefix or Suffix (default = '$'")
+    char substitutionEscapeCharacter() default DEFAULT_ESCAPE_CHARACTER;
+    @AttributeDefinition(
+        name = "Enable Substitution in Variables",
+        description = "Flag that indicates if substitution is allowed in Variables (default = false")
+    boolean substitutionInVariables() default DEFAULT_IN_VARIABLE_SUBSTITUTION;
+
+    @AttributeDefinition(
+        name = "Placeholder Values",
+        description = "A list of key / value pairs separated by a equal (=) sign. " +
+            "The key is not permitted to contain a '=' sign and the first occurrence of '=' " +
+            "separates the key from the value. If no '=' is found the entire key / value pair " +
+            "is dropped.")
+    String[] placeHolderKeyValuePairs() default {"phv.default.host.name=localhost"};
+}
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/mapping/StringInterpolationProviderImpl.java b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/StringInterpolationProviderImpl.java
new file mode 100644
index 0000000..25400f7
--- /dev/null
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/StringInterpolationProviderImpl.java
@@ -0,0 +1,143 @@
+/*
+ * 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 org.apache.commons.lang3.text.StrSubstitutor;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Modified;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.annotation.Annotation;
+import java.util.HashMap;
+import java.util.Map;
+
+@Designate(ocd = StringInterpolationProviderConfiguration.class)
+@Component(name = "org.apache.sling.resourceresolver.impl.mapping.StringInterpolationProvider")
+public class StringInterpolationProviderImpl
+    implements StringInterpolationProvider
+{
+    /** Logger. */
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    public static final StringInterpolationProviderConfiguration DEFAULT_CONFIG = new StringInterpolationProviderConfigurationImpl();
+
+    private Map<String, String> placeholderEntries = new HashMap<>();
+    private StrSubstitutor substitutor = new StrSubstitutor();
+
+    // ---------- SCR Integration ---------------------------------------------
+
+    /**
+     * Activates this component (called by SCR before)
+     */
+    @Activate
+    protected void activate(final StringInterpolationProviderConfiguration config) {
+        String prefix = config.substitutionPrefix();
+        String suffix = config.substitutionSuffix();
+        char escapeCharacter = config.substitutionEscapeCharacter();
+        boolean substitudeInVariables = config.substitutionInVariables();
+
+        String[] valueMap = config.placeHolderKeyValuePairs();
+        // Clear out any existing values
+        placeholderEntries.clear();
+        for(String line: valueMap) {
+            // Ignore no or empty lines
+            if(line != null && !line.isEmpty()) {
+                // Ignore comments
+                if(line.charAt(0) != '#') {
+                    int index = line.indexOf('=');
+                    if (index <= 0) {
+                        logger.warn("Placeholder Entry does not contain a key: '{}' -> ignored", line);
+                    } else if (index > line.length() - 2) {
+                        logger.warn("Placeholder Entry does not contain a value: '{}' -> ignored", line);
+                    } else {
+                        placeholderEntries.put(line.substring(0, index), line.substring(index + 1));
+                    }
+                }
+            }
+        }
+
+        substitutor = new StrSubstitutor(
+            placeholderEntries,
+            prefix,
+            suffix,
+            escapeCharacter
+        );
+        substitutor.setEnableSubstitutionInVariables(substitudeInVariables);
+    }
+
+    /**
+     * Modifies this component (called by SCR to update this component)
+     */
+    @Modified
+    protected void modified(final StringInterpolationProviderConfiguration config) {
+        this.activate(config);
+    }
+
+    /**
+     * Deactivates this component (called by SCR to take out of service)
+     */
+    @Deactivate
+    protected void deactivate() {
+        activate(DEFAULT_CONFIG);
+    }
+
+    @Override
+    public String substitute(String text) {
+        return substitutor.replace(text);
+    }
+
+    private static class StringInterpolationProviderConfigurationImpl
+        implements StringInterpolationProviderConfiguration
+    {
+        @Override
+        public String substitutionPrefix() {
+            return DEFAULT_PREFIX;
+        }
+
+        @Override
+        public String substitutionSuffix() {
+            return DEFAULT_SUFFIX;
+        }
+
+        @Override
+        public char substitutionEscapeCharacter() {
+            return DEFAULT_ESCAPE_CHARACTER;
+        }
+
+        @Override
+        public boolean substitutionInVariables() {
+            return DEFAULT_IN_VARIABLE_SUBSTITUTION;
+        }
+
+        @Override
+        public String[] placeHolderKeyValuePairs() {
+            return new String[0];
+        }
+
+        @Override
+        public Class<? extends Annotation> annotationType() {
+            return ObjectClassDefinition.class;
+        }
+    }
+}
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 b8f73e1..6460ff5 100644
--- a/src/test/java/org/apache/sling/resourceresolver/impl/EtcMappingResourceResolverTest.java
+++ b/src/test/java/org/apache/sling/resourceresolver/impl/EtcMappingResourceResolverTest.java
@@ -23,6 +23,8 @@
 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.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;
@@ -52,7 +54,9 @@
 import static org.apache.sling.resourceresolver.util.MockTestUtil.checkInternalResource;
 import static org.apache.sling.resourceresolver.util.MockTestUtil.checkRedirectResource;
 import static org.apache.sling.resourceresolver.util.MockTestUtil.createRequestFromUrl;
+import static org.apache.sling.resourceresolver.util.MockTestUtil.createStringInterpolationProviderConfiguration;
 import static org.apache.sling.resourceresolver.util.MockTestUtil.setInaccessibleField;
+import static org.apache.sling.resourceresolver.util.MockTestUtil.setupStringInterpolationProvider;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyString;
 import static org.mockito.Mockito.mock;
@@ -87,6 +91,9 @@
     @Mock
     ResourceProvider<?> resourceProvider;
 
+    StringInterpolationProviderConfiguration stringInterpolationProviderConfiguration;
+
+    StringInterpolationProviderImpl stringInterpolationProvider = new StringInterpolationProviderImpl();
     MapEntries mapEntries;
 
     File vanityBloomFilterFile;
@@ -115,6 +122,8 @@
         setInaccessibleField("resourceProviderTracker", activator, resourceProviderTracker);
         setInaccessibleField("resourceAccessSecurityTracker", activator, new ResourceAccessSecurityTracker());
         setInaccessibleField("bundleContext", activator, bundleContext);
+        stringInterpolationProviderConfiguration = createStringInterpolationProviderConfiguration();
+        setInaccessibleField("stringInterpolationProvider", activator, stringInterpolationProvider);
         setInaccessibleField("mapRoot", activator, "/etc/map");
         setInaccessibleField("mapRootPrefix", activator, "/etc/map");
         setInaccessibleField("observationPaths", activator, new Path[] {new Path("/")});
@@ -273,10 +282,46 @@
         checkInternalResource(resolvedResource, "/anecdotes/stories");
     }
 
+    @Test
+    public void simple_node_string_interpolation() throws Exception {
+        buildResource("${siv.one}", http, resourceResolver, resourceProvider,PROP_REDIRECT_EXTERNAL, "/content/simple-node");
+        setupStringInterpolationProvider(stringInterpolationProvider, stringInterpolationProviderConfiguration, new String[] {"siv.one=test-simple-node.80"});
+
+        refreshMapEntries("/etc/map", true);
+
+        ExpectedEtcMapping expectedEtcMapping = new ExpectedEtcMapping("^http/test-simple-node.80/", "/content/simple-node/");
+        expectedEtcMapping.assertEtcMap("String Interpolation for simple match", commonFactory.getMapEntries().getResolveMaps());
+
+        Resource content = buildResource("/content", null, resourceResolver, resourceProvider);
+        Resource simpleNode = buildResource("/content/simple-node", content, resourceResolver, resourceProvider);
+
+        HttpServletRequest request = createRequestFromUrl("http://test-simple-node:80/");
+        Resource resolvedResource = resourceResolver.resolve(request, "/");
+        checkRedirectResource(resolvedResource, "/content/simple-node/", 302);
+    }
+
+    @Test
+    public void simple_match_string_interpolation() throws Exception {
+        buildResource("test-node", http, resourceResolver, resourceProvider,
+            PROP_REG_EXP, "${siv.one}/",
+            PROP_REDIRECT_EXTERNAL, "/content/simple-match/"
+        );
+        setupStringInterpolationProvider(stringInterpolationProvider, stringInterpolationProviderConfiguration, new String[] {"siv.one=test-simple-match.80"});
+
+        refreshMapEntries("/etc/map", true);
+
+        ExpectedEtcMapping expectedEtcMapping = new ExpectedEtcMapping("^http/test-simple-match.80/", "/content/simple-match/");
+        expectedEtcMapping.assertEtcMap("String Interpolation for simple match", commonFactory.getMapEntries().getResolveMaps());
+
+        HttpServletRequest request = createRequestFromUrl("http://test-simple-match:80/");
+        Resource resolvedResource = resourceResolver.resolve(request, "/");
+        checkRedirectResource(resolvedResource, "/content/simple-match/", 302);
+    }
+
     /**
      * ATTENTION: this tests showcases an erroneous condition of an endless circular mapping in the /etc/map. When
      * this test passes this condition is present. After a fix this test must be adjusted.
-     * 
+     *
      * This confirms an issue with the Etc Mapping where a mapping from a node to a child node (here / to /content)
      * ends up in a endless circular mapping.
      * The only way to recover from this is to go to the OSGi console and change the /etc/map path in the Resource
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 953cd89..c0b4b69 100644
--- a/src/test/java/org/apache/sling/resourceresolver/impl/MockedResourceResolverImplTest.java
+++ b/src/test/java/org/apache/sling/resourceresolver/impl/MockedResourceResolverImplTest.java
@@ -17,6 +17,7 @@
  */
 package org.apache.sling.resourceresolver.impl;
 
+import static org.apache.sling.resourceresolver.util.MockTestUtil.getResourceName;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
@@ -399,7 +400,7 @@
     @SuppressWarnings("unchecked")
     private Resource buildResource(String fullpath, Iterable<Resource> children, ResourceResolver resourceResolver, ResourceProvider<?> provider, String ... properties) {
         Resource resource = Mockito.mock(Resource.class);
-        Mockito.when(resource.getName()).thenReturn(getName(fullpath));
+        Mockito.when(resource.getName()).thenReturn(getResourceName(fullpath));
         Mockito.when(resource.getPath()).thenReturn(fullpath);
         ResourceMetadata resourceMetadata = new ResourceMetadata();
         Mockito.when(resource.getResourceMetadata()).thenReturn(resourceMetadata);
@@ -428,15 +429,15 @@
     }
 
 
-    /**
-     * extract the name from a path.
-     * @param fullpath
-     * @return
-     */
-    private String getName(String fullpath) {
-        int n = fullpath.lastIndexOf("/");
-        return fullpath.substring(n+1);
-    }
+//    /**
+//     * extract the name from a path.
+//     * @param fullpath
+//     * @return
+//     */
+//    private String getName(String fullpath) {
+//        int n = fullpath.lastIndexOf("/");
+//        return fullpath.substring(n+1);
+//    }
 
     /**
      * Test getting a resolver.
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 6a5f9e2..44e04a9 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
@@ -46,6 +46,8 @@
 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;
@@ -78,6 +80,9 @@
     @Mock
     ResourceResolver resourceResolver;
 
+    StringInterpolationProviderConfiguration stringInterpolationProviderConfiguration;
+
+    StringInterpolationProviderImpl stringInterpolationProvider = new StringInterpolationProviderImpl();
     MapEntries mapEntries;
 
     File vanityBloomFilterFile;
@@ -111,7 +116,9 @@
         map = setupEtcMapResource("/etc", "map");
         http = setupEtcMapResource("http", map);
 
-        mapEntries = new MapEntries(resourceResolverFactory, bundleContext, eventAdmin);
+        stringInterpolationProviderConfiguration = createStringInterpolationProviderConfiguration();
+        setupStringInterpolationProvider(stringInterpolationProvider, stringInterpolationProviderConfiguration, new String[] {});
+        mapEntries = new MapEntries(resourceResolverFactory, bundleContext, eventAdmin, stringInterpolationProvider);
 
         final Field aliasMapField = MapEntries.class.getDeclaredField("aliasMap");
         aliasMapField.setAccessible(true);
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 a7d09ca..a98ca6e 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,12 +16,40 @@
  */
 package org.apache.sling.resourceresolver.impl.mapping;
 
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.path.Path;
+import org.apache.sling.resourceresolver.impl.CommonResourceResolverFactoryImpl;
+import org.apache.sling.resourceresolver.impl.ResourceAccessSecurityTracker;
+import org.apache.sling.resourceresolver.impl.ResourceResolverFactoryActivator;
+import org.apache.sling.resourceresolver.impl.ResourceResolverFactoryImpl;
+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.ResolveContext;
+import org.apache.sling.spi.resource.provider.ResourceContext;
+import org.apache.sling.spi.resource.provider.ResourceProvider;
+import org.junit.Test;
+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 org.apache.sling.api.resource.Resource;
-import org.apache.sling.resourceresolver.util.MockTestUtil.ExpectedEtcMapping;
-import org.junit.Test;
+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
@@ -99,5 +127,125 @@
             .addEtcMapEntry("^http/localhost\\.\\d*/gateway/", true, "http://gbiv.com/")
             .addEtcMapEntry("^http/localhost\\.\\d*/(stories)/", true, "/anecdotes/$1/");
         expectedEtcMapping.assertEtcMap("Etc Mapping for nested internal mixed mapping", mapEntries.getResolveMaps());
+
+        // Not really an etc-map resource but it is good for now
+        final Resource test = setupEtcMapResource("/scripts", "test");
+        ResourceProvider<?> rp = new ResourceProvider<Object>() {
+            @Override
+            public Resource getResource(ResolveContext<Object> ctx, String path, ResourceContext rCtx, Resource parent) {
+                if(path.equals("/scripts/test")) {
+                    return test;
+                }
+                if(path.startsWith(map.getPath())) {
+                    return findMapping(map, path);
+                }
+                return null;
+            }
+
+            private Resource findMapping(Resource parent, String path) {
+                if(parent.getPath().equals(path)) {
+                    return parent;
+                }
+                Iterator<Resource> i = parent.listChildren();
+                while(i.hasNext()) {
+                    Resource child = i.next();
+                    if(path.equals(child.getPath())) {
+                        return child;
+                    } else {
+                        return findMapping(child, path);
+                    }
+                }
+                return null;
+            }
+
+            @Override
+            public Iterator<Resource> listChildren(ResolveContext<Object> ctx, Resource parent) {
+                if(parent.getPath().startsWith(map.getPath())) {
+                    return parent.listChildren();
+                }
+                return null;
+            }
+        };
+
+        List<ResourceProviderHandler> handlers = asList(createRPHandler(rp, "rp1", 0, "/"));
+        ResourceProviderTracker resourceProviderTracker = mock(ResourceProviderTracker.class);
+        ResourceProviderStorage storage = new ResourceProviderStorage(handlers);
+        when(resourceProviderTracker.getResourceProviderStorage()).thenReturn(storage);
+        ResourceResolverFactoryActivator activator = spy(new ResourceResolverFactoryActivator());
+        // Both 'resourceProviderTracker' and 'resourceAccessSecurityTracker' are package private and so we cannot
+        // set them here. Intercept the call to obtain them and provide the desired value
+        when(activator.getResourceProviderTracker()).thenReturn(resourceProviderTracker);
+        when(activator.getResourceAccessSecurityTracker()).thenReturn(new ResourceAccessSecurityTracker());
+        when(activator.getBundleContext()).thenReturn(bundleContext);
+        when(activator.getStringInterpolationProvider()).thenReturn(stringInterpolationProvider);
+        when(activator.getMapRoot()).thenReturn("/etc/map");
+        when(activator.getObservationPaths()).thenReturn(new Path[] {new Path("/")});
+        CommonResourceResolverFactoryImpl commonFactory = spy(new CommonResourceResolverFactoryImpl(activator));
+        when(bundleContext.getBundle()).thenReturn(bundle);
+        ServiceUserMapper serviceUserMapper = mock(ServiceUserMapper.class);
+        when(activator.getServiceUserMapper()).thenReturn(serviceUserMapper);
+        when(serviceUserMapper.getServiceUserID(any(Bundle.class),anyString())).thenReturn("mapping");
+        Method method = CommonResourceResolverFactoryImpl.class.getDeclaredMethod("activate", BundleContext.class);
+        method.setAccessible(true);
+        method.invoke(commonFactory, bundleContext);
+        final Bundle usingBundle = mock(Bundle.class);
+        ResourceResolverFactoryImpl resFac = new ResourceResolverFactoryImpl(commonFactory, usingBundle, null);
+        ResourceResolver resResolver = resFac.getAdministrativeResourceResolver(null);
+
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getScheme()).thenReturn("http");
+        when(request.getServerName()).thenReturn("localhost");
+        when(request.getServerPort()).thenReturn(80);
+        Resource mappedResource = resResolver.resolve(request, "/cgi-bin/test.html");
+        String path = mappedResource.getPath();
+        assertEquals("Wrong Resolved Path", "/scripts/test", path);
     }
+
+//    @Test
+//    public void regex_map_internal_mapping() throws Exception {
+//        setupEtcMapResource("regexmap", http,
+//            PROP_REG_EXP, "$1.example.com/$2",
+//            PROP_REDIRECT_INTERNAL, "/content/([^/]+)/(.*)"
+//        );
+//
+//        mapEntries.doInit();
+//        // Regex Mappings are ignored for the Resolve Map
+//        ExpectedEtcMapping expectedEtcMapping = new ExpectedEtcMapping();
+////            .addEtcMapEntry("^http/$1.example.com/$2", true, "/content/([^/]+)/(.*)");
+//        expectedEtcMapping.assertEtcMap("Etc Mapping for regex map internal mapping", mapEntries.getResolveMaps());
+//
+//        ResourceProvider<?> rp = new ResourceProvider<Object>() {
+//
+//            @Override
+//            public Resource getResource(ResolveContext<Object> ctx, String path, ResourceContext rCtx, Resource parent) {
+//                return null;
+//            }
+//
+//            @Override
+//            public Iterator<Resource> listChildren(ResolveContext<Object> ctx, Resource parent) {
+//                return null;
+//            }
+//        };
+//
+//        List<ResourceProviderHandler> handlers = asList(createRPHandler(rp, "rp1", 0, "/"));
+//        ResourceProviderTracker resourceProviderTracker = mock(ResourceProviderTracker.class);
+//        ResourceProviderStorage storage = new ResourceProviderStorage(handlers);
+//        when(resourceProviderTracker.getResourceProviderStorage()).thenReturn(storage);
+//        ResourceResolverFactoryActivator activator = spy(new ResourceResolverFactoryActivator());
+//        when(activator.getResourceProviderTracker()).thenReturn(resourceProviderTracker);
+////        activator.resourceProviderTracker = resourceProviderTracker;
+//        when(activator.getResourceAccessSecurityTracker()).thenReturn(new ResourceAccessSecurityTracker());
+////        activator.resourceAccessSecurityTracker = new ResourceAccessSecurityTracker();
+//        CommonResourceResolverFactoryImpl commonFactory = new CommonResourceResolverFactoryImpl(activator);
+//        final Bundle usingBundle = mock(Bundle.class);
+//        ResourceResolverFactoryImpl resFac = new ResourceResolverFactoryImpl(commonFactory, usingBundle, null);
+//        ResourceResolver resResolver = resFac.getAdministrativeResourceResolver(null);
+//
+//        HttpServletRequest request = mock(HttpServletRequest.class);
+//        when(request.getScheme()).thenReturn("http");
+//        when(request.getServerName()).thenReturn("a.example.com");
+//        when(request.getServerPort()).thenReturn(80);
+//        Resource mappedResource = resResolver.resolve(request, "/b.html");
+//        String path = mappedResource.getPath();
+//    }
 }
diff --git a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/MapEntriesTest.java b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/MapEntriesTest.java
index 9b11f48..fc6691c 100644
--- a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/MapEntriesTest.java
+++ b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/MapEntriesTest.java
@@ -125,7 +125,7 @@
         when(resourceResolver.findResources(anyString(), eq("sql"))).thenReturn(
                 Collections.<Resource> emptySet().iterator());
 
-        mapEntries = new MapEntries(resourceResolverFactory, bundleContext, eventAdmin);
+        mapEntries = new MapEntries(resourceResolverFactory, bundleContext, eventAdmin, stringInterpolationProvider);
         final Field aliasMapField = MapEntries.class.getDeclaredField("aliasMap");
         aliasMapField.setAccessible(true);
 
@@ -815,7 +815,7 @@
         addResource.setAccessible(true);
 
         when(resourceResolverFactory.isOptimizeAliasResolutionEnabled()).thenReturn(false);
-        mapEntries = new MapEntries(resourceResolverFactory, bundleContext, eventAdmin);
+        mapEntries = new MapEntries(resourceResolverFactory, bundleContext, eventAdmin, stringInterpolationProvider);
 
         Resource parent = mock(Resource.class);
         when(parent.getPath()).thenReturn("/parent");
@@ -840,7 +840,7 @@
         addResource.setAccessible(true);
 
         when(resourceResolverFactory.isOptimizeAliasResolutionEnabled()).thenReturn(false);
-        mapEntries = new MapEntries(resourceResolverFactory, bundleContext, eventAdmin);
+        mapEntries = new MapEntries(resourceResolverFactory, bundleContext, eventAdmin, stringInterpolationProvider);
 
         Resource parent = mock(Resource.class);
         when(parent.getPath()).thenReturn("/parent");
@@ -865,7 +865,7 @@
         removeAlias.setAccessible(true);
 
         when(resourceResolverFactory.isOptimizeAliasResolutionEnabled()).thenReturn(false);
-        mapEntries = new MapEntries(resourceResolverFactory, bundleContext, eventAdmin);
+        mapEntries = new MapEntries(resourceResolverFactory, bundleContext, eventAdmin, stringInterpolationProvider);
 
         Resource parent = mock(Resource.class);
         when(parent.getPath()).thenReturn("/parent");
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 58263b6..479ff0e 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
@@ -81,7 +81,8 @@
 
         ctx.registerInjectActivateService(new ServiceUserMapperImpl());
         ctx.registerInjectActivateService(new ResourceAccessSecurityTracker());
-        
+        ctx.registerInjectActivateService(new StringInterpolationProviderImpl());
+
         InMemoryResourceProvider resourceProvider = new InMemoryResourceProvider();
         resourceProvider.putResource("/"); // root
         resourceProvider.putResource("/here"); // regular page
diff --git a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/StringInterpolationMapEntriesTest.java b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/StringInterpolationMapEntriesTest.java
new file mode 100644
index 0000000..9aa5d05
--- /dev/null
+++ b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/StringInterpolationMapEntriesTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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 org.apache.sling.api.resource.Resource;
+import org.junit.Test;
+
+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.setupStringInterpolationProvider;
+
+/**
+ * These are tests that are testing the Sling Interpolation Feature (SLING-7768)
+ * on the MapEntries level
+ */
+public class StringInterpolationMapEntriesTest extends AbstractMappingMapEntriesTest {
+
+    @Test
+    public void simple_node_string_interpolation() throws Exception {
+        // To avoid side effects the String Interpolation uses its own Resource Resolver
+        Resource sivOne = setupEtcMapResource("${siv.one}", http,PROP_REDIRECT_EXTERNAL, "/content/simple-node");
+        setupStringInterpolationProvider(stringInterpolationProvider, stringInterpolationProviderConfiguration, new String[] {"siv.one=test-simple-node"});
+
+        mapEntries.doInit();
+        ExpectedEtcMapping expectedEtcMapping = new ExpectedEtcMapping("^http/test-simple-node/", "/content/simple-node/");
+        expectedEtcMapping.assertEtcMap("String Interpolation for simple match", mapEntries.getResolveMaps());
+    }
+
+    @Test
+    public void simple_match_string_interpolation() throws Exception {
+        // To avoid side effects the String Interpolation uses its own Resource Resolver
+        Resource sivOne = setupEtcMapResource("test-node", http,
+            PROP_REG_EXP, "${siv.one}/",
+            PROP_REDIRECT_EXTERNAL, "/content/simple-match/"
+        );
+        setupStringInterpolationProvider(stringInterpolationProvider, stringInterpolationProviderConfiguration, new String[] {"siv.one=test-simple-match"});
+
+        mapEntries.doInit();
+        ExpectedEtcMapping expectedEtcMapping = new ExpectedEtcMapping("^http/test-simple-match/", "/content/simple-match/");
+        expectedEtcMapping.assertEtcMap("String Interpolation for simple match", mapEntries.getResolveMaps());
+    }
+
+//    @Test
+//    public void simple_nested_match_string_interpolation() throws Exception {
+//        // To avoid side effects the String Interpolation uses its own Resource Resolver
+//        Resource sivOne = setupEtcMapResource("test-node", http,
+//            PROP_REG_EXP, "${siv.one}/",
+//            PROP_REDIRECT_EXTERNAL, "/content/simple-match/"
+//        );
+//        setupStringInterpolationProvider(new String[] {"siv.one=test-simple-match"});
+//
+//        mapEntries.doInit();
+//        ExpectedEtcMapping expectedEtcMapping = new ExpectedEtcMapping("^http/test-simple-match/", "/content/simple-match/");
+//        expectedEtcMapping.assertEtcMap("String Interpolation for simple match", mapEntries.getResolveMaps());
+//    }
+}
diff --git a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/StringInterpolationProviderImplTest.java b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/StringInterpolationProviderImplTest.java
new file mode 100644
index 0000000..ff651ed
--- /dev/null
+++ b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/StringInterpolationProviderImplTest.java
@@ -0,0 +1,334 @@
+/*
+ * 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 org.apache.commons.lang3.text.StrSubstitutor;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.osgi.framework.BundleContext;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.apache.sling.resourceresolver.impl.mapping.StringInterpolationProviderImpl.DEFAULT_CONFIG;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.when;
+
+public class StringInterpolationProviderImplTest {
+
+    @Mock
+    private BundleContext bundleContext;
+
+    @Mock
+    private StringInterpolationProviderConfiguration stringInterpolationProviderConfiguration;
+
+    @SuppressWarnings({ "unchecked" })
+    @Before
+    public void setup() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        when(stringInterpolationProviderConfiguration.substitutionPrefix()).thenReturn("${");
+        when(stringInterpolationProviderConfiguration.substitutionSuffix()).thenReturn("}");
+        when(stringInterpolationProviderConfiguration.substitutionEscapeCharacter()).thenReturn('$');
+        when(stringInterpolationProviderConfiguration.substitutionInVariables()).thenReturn(false);
+    }
+
+    @Test
+    public void test_strsubstitutor() {
+        Map<String,String> values = new HashMap<>();
+        values.put("one", "two");
+        StrSubstitutor substitutor = new StrSubstitutor(values, "${", "}", '$');
+        String substitude = substitutor.replace("${one}");
+        assertEquals("Wrong Replacement", "two", substitude);
+    }
+
+    @Test
+    public void test_simple_one_placeholder() {
+        when(stringInterpolationProviderConfiguration.placeHolderKeyValuePairs()).thenReturn(
+            new String[] { "one=two"}
+        );
+
+        StringInterpolationProviderImpl placeholderProvider = new StringInterpolationProviderImpl();
+        placeholderProvider.activate(stringInterpolationProviderConfiguration);
+
+        String line = "${one}";
+        String substituted = placeholderProvider.substitute(line);
+        assertEquals("Wrong resolved line", "two", substituted);
+    }
+
+    @Test
+    public void test_simple_text_one_placeholder() {
+        when(stringInterpolationProviderConfiguration.placeHolderKeyValuePairs()).thenReturn(
+            new String[] { "one=two"}
+        );
+        StringInterpolationProviderImpl placeholderProvider = new StringInterpolationProviderImpl();
+        placeholderProvider.activate(stringInterpolationProviderConfiguration);
+
+        String line = "Here is ${one}, too";
+        String substituted = placeholderProvider.substitute(line);
+        assertEquals("Wrong resolved line", "Here is two, too", substituted);
+    }
+
+    @Test
+    public void test_two_placeholders() {
+        when(stringInterpolationProviderConfiguration.placeHolderKeyValuePairs()).thenReturn(
+            new String[] { "one=two", "three=four"}
+        );
+        StringInterpolationProviderImpl placeholderProvider = new StringInterpolationProviderImpl();
+        placeholderProvider.activate(stringInterpolationProviderConfiguration);
+
+        String line = "${one} with another ${three}";
+        String substituted = placeholderProvider.substitute(line);
+        assertEquals("Wrong resolved line", "two with another four", substituted);
+    }
+
+    @Test
+    public void test_three_placeholders() {
+        when(stringInterpolationProviderConfiguration.placeHolderKeyValuePairs()).thenReturn(
+            new String[] { "one=two", "three=four", "five=six"}
+        );
+
+        StringInterpolationProviderImpl placeholderProvider = new StringInterpolationProviderImpl();
+        placeholderProvider.activate(stringInterpolationProviderConfiguration);
+
+        String line = "Here comes ${one} with another ${three} equals ${five}, horray!";
+        String substituted = placeholderProvider.substitute(line);
+        assertEquals("Wrong resolved line", "Here comes two with another four equals six, horray!", substituted);
+    }
+
+    @Test
+    public void test_no_placeholders() {
+        when(stringInterpolationProviderConfiguration.placeHolderKeyValuePairs()).thenReturn(
+            new String[] { "one=two", "three=four", "five=six"}
+        );
+
+        StringInterpolationProviderImpl placeholderProvider = new StringInterpolationProviderImpl();
+        placeholderProvider.activate(stringInterpolationProviderConfiguration);
+
+        String line = "Here comes is a text with no placeholders!";
+        String substituted = placeholderProvider.substitute(line);
+        assertEquals("Wrong resolved line", "Here comes is a text with no placeholders!", substituted);
+    }
+
+    @Test
+    public void test_unkown_placeholders() {
+        when(stringInterpolationProviderConfiguration.placeHolderKeyValuePairs()).thenReturn(
+            new String[] { "one=two", "three=four", "five=six"}
+        );
+
+        StringInterpolationProviderImpl placeholderProvider = new StringInterpolationProviderImpl();
+        placeholderProvider.activate(stringInterpolationProviderConfiguration);
+
+        String line = "Here comes ${unkown} placeholders!";
+        String substituted = placeholderProvider.substitute(line);
+        assertEquals("Wrong resolved line", "Here comes ${unkown} placeholders!", substituted);
+    }
+
+    @Test
+    public void test_trailing_slash_placeholders() {
+        when(stringInterpolationProviderConfiguration.placeHolderKeyValuePairs()).thenReturn(
+            new String[] { "siv.one=test-value"}
+        );
+
+        StringInterpolationProviderImpl placeholderProvider = new StringInterpolationProviderImpl();
+        placeholderProvider.activate(stringInterpolationProviderConfiguration);
+
+        String line = "${siv.one}/";
+        String substituted = placeholderProvider.substitute(line);
+        assertEquals("Wrong resolved line", "test-value/", substituted);
+    }
+
+    @Test
+    public void test_different_suffix_prefix() {
+        when(stringInterpolationProviderConfiguration.placeHolderKeyValuePairs()).thenReturn(
+            new String[] { "test-me.one=hello"}
+        );
+        when(stringInterpolationProviderConfiguration.substitutionPrefix()).thenReturn("{{");
+        when(stringInterpolationProviderConfiguration.substitutionSuffix()).thenReturn("}}");
+
+        StringInterpolationProviderImpl placeholderProvider = new StringInterpolationProviderImpl();
+        placeholderProvider.activate(stringInterpolationProviderConfiguration);
+
+        String line = "a-{{test-me.one}}-a";
+        String substituted = placeholderProvider.substitute(line);
+        assertEquals("Wrong resolved line", "a-hello-a", substituted);
+    }
+
+    @Test
+    public void test_escape_character() {
+        when(stringInterpolationProviderConfiguration.placeHolderKeyValuePairs()).thenReturn(
+            new String[] { "one=two"}
+        );
+        when(stringInterpolationProviderConfiguration.substitutionEscapeCharacter()).thenReturn('\\');
+
+        StringInterpolationProviderImpl placeholderProvider = new StringInterpolationProviderImpl();
+        placeholderProvider.activate(stringInterpolationProviderConfiguration);
+
+        String line = "\\${one}=${one}";
+        String substituted = placeholderProvider.substitute(line);
+        assertEquals("Wrong resolved line", "${one}=two", substituted);
+    }
+
+    @Test
+    public void test_in_variables_substitution() {
+        when(stringInterpolationProviderConfiguration.placeHolderKeyValuePairs()).thenReturn(
+            new String[] { "one=two", "two=three"}
+        );
+        when(stringInterpolationProviderConfiguration.substitutionInVariables()).thenReturn(true);
+
+        StringInterpolationProviderImpl placeholderProvider = new StringInterpolationProviderImpl();
+        placeholderProvider.activate(stringInterpolationProviderConfiguration);
+
+        String line = "${${one}}";
+        String substituted = placeholderProvider.substitute(line);
+        assertEquals("Wrong resolved line", "three", substituted);
+    }
+
+    @Test
+    public void test_in_variables_substitution2() {
+        when(stringInterpolationProviderConfiguration.placeHolderKeyValuePairs()).thenReturn(
+            new String[] { "one=two", "onetwo=three"}
+        );
+        when(stringInterpolationProviderConfiguration.substitutionInVariables()).thenReturn(true);
+
+        StringInterpolationProviderImpl placeholderProvider = new StringInterpolationProviderImpl();
+        placeholderProvider.activate(stringInterpolationProviderConfiguration);
+
+        String line = "${one${one}}";
+        String substituted = placeholderProvider.substitute(line);
+        assertEquals("Wrong resolved line", "three", substituted);
+    }
+
+    @Test
+    public void test_bad_placeholder_key_configuration() {
+        when(stringInterpolationProviderConfiguration.placeHolderKeyValuePairs()).thenReturn(
+            new String[] { "one=two", "two", "=two"}
+        );
+        when(stringInterpolationProviderConfiguration.substitutionInVariables()).thenReturn(true);
+
+        StringInterpolationProviderImpl placeholderProvider = new StringInterpolationProviderImpl();
+        placeholderProvider.activate(stringInterpolationProviderConfiguration);
+
+        String line = "${one}${two}";
+        String substituted = placeholderProvider.substitute(line);
+        assertEquals("Wrong resolved line", "two${two}", substituted);
+    }
+
+    @Test
+    public void test_bad_placeholder_value_configuration() {
+        when(stringInterpolationProviderConfiguration.placeHolderKeyValuePairs()).thenReturn(
+            new String[] { "one=two", "two="}
+        );
+        when(stringInterpolationProviderConfiguration.substitutionInVariables()).thenReturn(true);
+
+        StringInterpolationProviderImpl placeholderProvider = new StringInterpolationProviderImpl();
+        placeholderProvider.activate(stringInterpolationProviderConfiguration);
+
+        String line = "${one}${two}";
+        String substituted = placeholderProvider.substitute(line);
+        assertEquals("Wrong resolved line", "two${two}", substituted);
+    }
+
+    @Test
+    public void test_comment_in_configuration() {
+        when(stringInterpolationProviderConfiguration.placeHolderKeyValuePairs()).thenReturn(
+            new String[] { "one=two", "# Next One", "two=four"}
+        );
+        when(stringInterpolationProviderConfiguration.substitutionInVariables()).thenReturn(true);
+
+        StringInterpolationProviderImpl placeholderProvider = new StringInterpolationProviderImpl();
+        placeholderProvider.activate(stringInterpolationProviderConfiguration);
+
+        String line = "${one}-${two}";
+        String substituted = placeholderProvider.substitute(line);
+        assertEquals("Wrong resolved line", "two-four", substituted);
+    }
+
+    @Test
+    public void test_empty_line_in_configuration() {
+        when(stringInterpolationProviderConfiguration.placeHolderKeyValuePairs()).thenReturn(
+            new String[] { "one=two", "", "two=four"}
+        );
+        when(stringInterpolationProviderConfiguration.substitutionInVariables()).thenReturn(true);
+
+        StringInterpolationProviderImpl placeholderProvider = new StringInterpolationProviderImpl();
+        placeholderProvider.activate(stringInterpolationProviderConfiguration);
+
+        String line = "${one}-${two}";
+        String substituted = placeholderProvider.substitute(line);
+        assertEquals("Wrong resolved line", "two-four", substituted);
+    }
+
+    @Test
+    public void test_no_configuration() {
+        StringInterpolationProviderImpl placeholderProvider = new StringInterpolationProviderImpl();
+
+        String line = "${one}${two}";
+        String substituted = placeholderProvider.substitute(line);
+        assertEquals("Wrong resolved line", line, substituted);
+    }
+
+    @Test
+    public void test_default_configuration() {
+        StringInterpolationProviderImpl placeholderProvider = new StringInterpolationProviderImpl();
+        placeholderProvider.activate(DEFAULT_CONFIG);
+
+        String line = "${one}${two}";
+        String substituted = placeholderProvider.substitute(line);
+        assertEquals("Wrong resolved line", line, substituted);
+    }
+
+    @Test
+    public void test_modify_configuration() {
+        when(stringInterpolationProviderConfiguration.placeHolderKeyValuePairs()).thenReturn(
+            new String[] { "one=two", "two="}
+        );
+        when(stringInterpolationProviderConfiguration.substitutionInVariables()).thenReturn(true);
+
+        StringInterpolationProviderImpl placeholderProvider = new StringInterpolationProviderImpl();
+        placeholderProvider.activate(stringInterpolationProviderConfiguration);
+        when(stringInterpolationProviderConfiguration.placeHolderKeyValuePairs()).thenReturn(
+            new String[] { "one=two", "two=three"}
+        );
+        placeholderProvider.modified(stringInterpolationProviderConfiguration);
+
+        String line = "${one}-${two}";
+        String substituted = placeholderProvider.substitute(line);
+        assertEquals("Wrong resolved line", "two-three", substituted);
+    }
+
+    @Test
+    public void test_deactivate_configuration() {
+        when(stringInterpolationProviderConfiguration.placeHolderKeyValuePairs()).thenReturn(
+            new String[] { "one=two", "two=four"}
+        );
+        when(stringInterpolationProviderConfiguration.substitutionInVariables()).thenReturn(true);
+
+        StringInterpolationProviderImpl placeholderProvider = new StringInterpolationProviderImpl();
+        placeholderProvider.activate(stringInterpolationProviderConfiguration);
+
+        String line = "${one}-${two}";
+        String substituted = placeholderProvider.substitute(line);
+        assertEquals("Wrong resolved line", "two-four", substituted);
+        placeholderProvider.deactivate();
+        substituted = placeholderProvider.substitute(line);
+        assertEquals("Line should not be substituted because service was deactivated", line, substituted);
+    }
+}
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 1480397..1dbfb8d 100644
--- a/src/test/java/org/apache/sling/resourceresolver/util/MockTestUtil.java
+++ b/src/test/java/org/apache/sling/resourceresolver/util/MockTestUtil.java
@@ -26,6 +26,8 @@
 import org.apache.sling.resourceresolver.impl.SimpleValueMapImpl;
 import org.apache.sling.resourceresolver.impl.helper.RedirectResource;
 import org.apache.sling.resourceresolver.impl.mapping.MapEntry;
+import org.apache.sling.resourceresolver.impl.mapping.StringInterpolationProvider;
+import org.apache.sling.resourceresolver.impl.mapping.StringInterpolationProviderConfiguration;
 import org.apache.sling.spi.resource.provider.ResolveContext;
 import org.apache.sling.spi.resource.provider.ResourceContext;
 import org.apache.sling.spi.resource.provider.ResourceProvider;
@@ -43,6 +45,10 @@
 import java.util.List;
 
 import static java.util.Arrays.asList;
+import static org.apache.sling.resourceresolver.impl.mapping.StringInterpolationProviderConfiguration.DEFAULT_ESCAPE_CHARACTER;
+import static org.apache.sling.resourceresolver.impl.mapping.StringInterpolationProviderConfiguration.DEFAULT_IN_VARIABLE_SUBSTITUTION;
+import static org.apache.sling.resourceresolver.impl.mapping.StringInterpolationProviderConfiguration.DEFAULT_PREFIX;
+import static org.apache.sling.resourceresolver.impl.mapping.StringInterpolationProviderConfiguration.DEFAULT_SUFFIX;
 import static org.hamcrest.Matchers.instanceOf;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThat;
@@ -302,6 +308,26 @@
         }
     }
 
+    public static StringInterpolationProviderConfiguration createStringInterpolationProviderConfiguration() {
+        StringInterpolationProviderConfiguration answer = mock(StringInterpolationProviderConfiguration.class);
+        when(answer.substitutionPrefix()).thenReturn(DEFAULT_PREFIX);
+        when(answer.substitutionSuffix()).thenReturn(DEFAULT_SUFFIX);
+        when(answer.substitutionEscapeCharacter()).thenReturn(DEFAULT_ESCAPE_CHARACTER);
+        when(answer.substitutionInVariables()).thenReturn(DEFAULT_IN_VARIABLE_SUBSTITUTION);
+        when(answer.placeHolderKeyValuePairs()).thenReturn(new String[] {});
+        return answer;
+    }
+
+    public static void setupStringInterpolationProvider(
+        StringInterpolationProvider provider, StringInterpolationProviderConfiguration configuration, final String[] placeholderValues
+    ) {
+        when(configuration.placeHolderKeyValuePairs()).thenReturn(placeholderValues);
+        callInaccessibleMethod("activate", Void.TYPE, provider,
+            new Class[] {StringInterpolationProviderConfiguration.class},
+            new Object[] {configuration}
+        );
+    }
+
     public static class FieldWrapper<T> {
         private Field field;
         private Object target;