Merge pull request #15 from apache/feature/SLING-7768

Feature/sling 7768: String Interpolation for ETC Map
diff --git a/README.md b/README.md
index 02bf7bc..c84dca7 100644
--- a/README.md
+++ b/README.md
@@ -7,3 +7,73 @@
 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 this it is possible to add placeholders to **sling:match** entries
+to make it possible to have them work for different environments.
+The values are either provides by System, Bundle Context or String
+Interpolation Configuration values.
+The placeholders have this format: **$['type':'name';default='default value']**.
+The type is can be:
+* **env**: take from the System Properties
+* **prop**: taken from the Bundle Context Properties
+* otherwise: taken from the String Interpolation Configuration
+
+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.
+**Note**: the placeholder **must be placed** into a **sling:match** entry
+and cannot be the JCR Node Name as some of the characters are not allowed.
+
+### 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
+            * my-mapping
+                * sling:match=$\[phv.fq.host.name\].8080
+            
+Opening the page **http://localhost:8080/starter/index.html** should
+work just fine.
+This is a mapping from System Properties with a default:
+* etc
+    * map
+        * http
+            * my-mapping
+                * sling:match=$\[env:phv.fq.host.name;default=localhost\].8080
+
+### 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/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..646a077 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;
 
+    /** String Interpolation Provider. */
+    @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/Interpolator.java b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/Interpolator.java
new file mode 100755
index 0000000..afa5147
--- /dev/null
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/Interpolator.java
@@ -0,0 +1,157 @@
+/*
+ * 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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Replace place holders in a string
+ */
+public class Interpolator {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(Interpolator.class);
+
+    public static final char END = ']';
+
+    public static final String START = "$[";
+
+    public static final char NAME_SEPARATOR = ':';
+    public static final char DIRECTIVES_SEPARATOR = ';';
+    public static final char DIRECTIVES_VALUE_SEPARATOR = '=';
+
+    public static final char ESCAPE = '\\';
+
+    /**
+     * The value for the replacement is returned by this provider
+     */
+    @FunctionalInterface
+    public static interface Provider {
+
+        Object provide(String type, String name, Map<String, String> directives);
+    }
+
+    /**
+     * Replace all place holders
+     *
+     * @param value    Value with place holders
+     * @param provider Provider for providing the values
+     * @return Replaced object (or original value)
+     */
+    public static Object replace(final String value, final Provider provider) {
+        String result = value;
+        int start = -1;
+        while (start < result.length()) {
+            start = result.indexOf(START, start);
+            if (start == -1) {
+                // no placeholder found -> end
+                LOGGER.trace("No Start ({}) found in: '{}'", START, result);
+                start = result.length();
+                continue;
+            }
+
+            boolean replace = true;
+            if (start > 0 && result.charAt(start - 1) == ESCAPE) {
+                if (start == 1 || result.charAt(start - 2) != ESCAPE) {
+                    LOGGER.trace("Escape ({}) found in: '{}'", ESCAPE, result);
+                    replace = false;
+                }
+            }
+
+            if (!replace) {
+                // placeholder is escaped -> remove placeholder and continue
+                result = result.substring(0, start - 1).concat(result.substring(start));
+                start = start + START.length();
+                continue;
+            }
+
+            int count = 1;
+            int index = start + START.length();
+            while (index < result.length() && count > 0) {
+                if (result.charAt(index) == START.charAt(1) && result.charAt(index - 1) == START.charAt(0)) {
+                    count++;
+                } else if (result.charAt(index) == END) {
+                    count--;
+                }
+                index++;
+            }
+
+            if (count > 0) {
+                LOGGER.trace("No End ({}) found in: '{}' (count: '{}')", END, result, count);
+                // no matching end found -> end
+                start = result.length();
+                continue;
+            }
+
+            final String key = result.substring(start + START.length(), index - 1);
+            final int sep = key.indexOf(NAME_SEPARATOR);
+            if (sep == -1) {
+                // invalid key
+                start = index;
+                continue;
+            }
+
+            final String type = key.substring(0, sep);
+            final String postfix = key.substring(sep + 1);
+            LOGGER.trace("Type: '{}', postfix: '{}'", type, postfix);
+
+            final int dirPos = postfix.indexOf(DIRECTIVES_SEPARATOR);
+            final Map<String, String> directives;
+            final String name;
+            if (dirPos == -1) {
+                name = postfix;
+                directives = Collections.emptyMap();
+                LOGGER.trace("No Directives");
+            } else {
+                name = postfix.substring(0, dirPos);
+                directives = new HashMap<>();
+
+                for (String dir : postfix.substring(dirPos + 1).split(DIRECTIVES_SEPARATOR + "")) {
+                    String[] kv = dir.split(DIRECTIVES_VALUE_SEPARATOR + "");
+                    if (kv.length == 2) {
+                        directives.put(kv[0], kv[1]);
+                    }
+                }
+                LOGGER.trace("Defaults: '{}'", directives);
+            }
+
+            // recursive replacement
+            final Object newName = replace(name, provider);
+
+            Object replacement = provider.provide(type, newName.toString(), directives);
+            if (replacement == null) {
+                // no replacement found -> leave as is and continue
+                LOGGER.trace("No Replacements found for: '{}'", newName);
+                start = index;
+            } else {
+                if (!(replacement instanceof String)) {
+                    if (start == 0 && index == result.length()) {
+                        return replacement;
+                    }
+                }
+                // replace and continue with replacement
+                result = result.substring(0, start).concat(replacement.toString()).concat(result.substring(index));
+                LOGGER.trace("Replacements found for: '{}': '{}'", newName, result);
+            }
+        }
+        return result;
+    }
+}
\ No newline at end of file
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..cdc5bcc 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 final 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..56fc598
--- /dev/null
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/StringInterpolationProviderConfiguration.java
@@ -0,0 +1,37 @@
+/*
+ * 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 key/value pairs"
+)
+public @interface StringInterpolationProviderConfiguration {
+
+    @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 as the first occurrence of '=' " +
+            "separates the key from the value. If no '=' is found the entry " +
+            "is ignored")
+    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..9ae9351
--- /dev/null
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/StringInterpolationProviderImpl.java
@@ -0,0 +1,130 @@
+/*
+ * 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.framework.BundleContext;
+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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Designate(ocd = StringInterpolationProviderConfiguration.class)
+@Component
+public class StringInterpolationProviderImpl
+    implements StringInterpolationProvider
+{
+    private static final String TYPE_ENV = "env";
+
+    private static final String TYPE_PROP = "prop";
+
+    private static final String TYPE_CONFIG = "config";
+
+    private static final String DIRECTIVE_DEFAULT = "default";
+
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    private Map<String, String> placeholderEntries = new HashMap<>();
+    private BundleContext context;
+
+    // ---------- SCR Integration ---------------------------------------------
+
+    /**
+     * Activates this component (called by SCR before)
+     */
+    @Activate
+    protected void activate(final BundleContext bundleContext, final StringInterpolationProviderConfiguration config) {
+        this.context = bundleContext;
+
+        String[] valueMap = config.placeHolderKeyValuePairs();
+        Map<String, String> newMap = new HashMap<>();
+        for(String line: valueMap) {
+            // Ignore no lines, empty lines and comments
+            if(line != null && !line.isEmpty() && 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 {
+                    newMap.put(line.substring(0, index), line.substring(index + 1));
+                }
+            }
+        }
+        this.placeholderEntries = newMap;
+    }
+
+    /**
+     * Modifies this component (called by SCR to update this component)
+     */
+    @Modified
+    protected void modified(final BundleContext bundleContext, final StringInterpolationProviderConfiguration config) {
+        this.activate(bundleContext, config);
+    }
+
+    /**
+     * Deactivates this component (called by SCR to take out of service)
+     */
+    @Deactivate
+    protected void deactivate(final BundleContext bundleContext) {
+        this.context = null;
+        this.placeholderEntries = new HashMap<>();
+    }
+
+    /**
+     * This is the method that is used by the Map Entries service to substitute values with
+     * the proper format
+     * @param text Text to be converted
+     * @return Should be either the substituted text or the original given text
+     */
+    @Override
+    public String substitute(String text) {
+        logger.trace("Substitute: '{}'", text);
+        Object result = Interpolator.replace(text, (type, name, dir) -> {
+            String v = null;
+            if (TYPE_ENV.equals(type)) {
+                v = getVariableFromEnvironment(name);
+            } else if (TYPE_PROP.equals(type)) {
+                v = getVariableFromProperty(name);
+            } else if(TYPE_CONFIG.equals(type)){
+                v = getVariableFromBundleConfiguration(name);
+            }
+            if (v == null) {
+                v = dir.get(DIRECTIVE_DEFAULT);
+            }
+            logger.trace("Return substitution value: '{}'", v);
+            return v;
+        });
+        logger.trace("Substitute result: '{}'", result);
+        return result == null ? null : result.toString();
+    }
+
+    String getVariableFromEnvironment(final String name) {
+        return System.getenv(name);
+    }
+
+    String getVariableFromProperty(final String name) { return context == null ? null : context.getProperty(name); }
+
+    String getVariableFromBundleConfiguration(final String name) { return placeholderEntries.get(name); }
+}
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..ca2d6a6 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,9 @@
 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;
@@ -52,7 +55,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 +92,9 @@
     @Mock
     ResourceProvider<?> resourceProvider;
 
+    StringInterpolationProviderConfiguration stringInterpolationProviderConfiguration;
+
+    StringInterpolationProvider stringInterpolationProvider = new StringInterpolationProviderImpl();
     MapEntries mapEntries;
 
     File vanityBloomFilterFile;
@@ -115,6 +123,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 +283,46 @@
         checkInternalResource(resolvedResource, "/anecdotes/stories");
     }
 
+    @Test
+    public void simple_node_string_interpolation() throws Exception {
+        buildResource("$[config: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, "$[config: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..b960a82 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);
@@ -427,17 +428,6 @@
         return resource;
     }
 
-
-    /**
-     * 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.
      * @throws LoginException
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/InterpolatorTest.java b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/InterpolatorTest.java
new file mode 100644
index 0000000..67b89cf
--- /dev/null
+++ b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/InterpolatorTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class InterpolatorTest {
+
+    private static final String TYPE_CONFIG = "config";
+
+    private static final String DIRECTIVE_DEFAULT = "default";
+
+    @Test
+    public void test_unclosed_placeholder() {
+        String match = "$[config:test;default=one$[config:another;default=two]";
+        Object answer = Interpolator.replace(
+            match,
+            (type, name, dir) -> {
+                String v = null;
+                if (TYPE_CONFIG.equals(type)){
+                    v = getVariableFromBundleConfiguration(name);
+                }
+                if (v == null) {
+                    v = dir.get(DIRECTIVE_DEFAULT);
+                }
+                return v;
+            }
+        );
+        assertTrue("Answer must be a string", answer instanceof String);
+        assertEquals("Nothing should have been changed", match, answer);
+    }
+
+    @Test
+    public void test_no_type() {
+        String match = "$[test;default=one]";
+        Object answer = Interpolator.replace(
+            match,
+            (type, name, dir) -> {
+                String v = null;
+                if (TYPE_CONFIG.equals(type)){
+                    v = getVariableFromBundleConfiguration(name);
+                }
+                if (v == null) {
+                    v = dir.get(DIRECTIVE_DEFAULT);
+                }
+                return v;
+            }
+        );
+        assertTrue("Answer must be a string", answer instanceof String);
+        assertEquals("Nothing should have been changed", match, answer);
+    }
+
+    @Test
+    public void test_not_a_string() {
+        String match = "$[config:test]";
+        Object answer = Interpolator.replace(
+            match,
+            (type, name, dir) -> {
+                Object v = null;
+                if (TYPE_CONFIG.equals(type)){
+                    v = new Integer(1);
+                }
+                return v;
+            }
+        );
+        assertTrue("Answer must be a Integer", answer instanceof Integer);
+        assertEquals("Nothing should have been changed", 1, answer);
+    }
+
+    private String getVariableFromBundleConfiguration(String name) {
+        return "'" + name + "'";
+    }
+}
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..9f04b6f
--- /dev/null
+++ b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/StringInterpolationMapEntriesTest.java
@@ -0,0 +1,56 @@
+/*
+ * 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("$[config: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, "$[config: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());
+    }
+}
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..60fe5e9
--- /dev/null
+++ b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/StringInterpolationProviderImplTest.java
@@ -0,0 +1,344 @@
+/*
+ * 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.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);
+    }
+
+    @Test
+    public void test_interpolator_simple() {
+        Map<String,String> values = new HashMap<>();
+        values.put("one", "two");
+        String substitute = interpolate("$[config:one]", values);
+        assertEquals("Wrong Replacement", "two", substitute);
+    }
+
+    @Test
+    public void test_interpolator_with_type() {
+        Map<String,String> values = new HashMap<>();
+        values.put("one", "two");
+        String substitute = interpolate("$[config:one]", values);
+        assertEquals("Wrong Replacement (with type)", "two", substitute);
+    }
+
+    @Test
+    public void test_interpolator_no_match() {
+        Map<String,String> values = new HashMap<>();
+        values.put("one", "two");
+        String substitute = interpolate("$[config:two]", values);
+        assertEquals("Should not been replaced", "$[config:two]", substitute);
+    }
+
+    @Test
+    public void test_interpolator_no_match_with_default() {
+        Map<String,String> values = new HashMap<>();
+        values.put("one", "two");
+        String substitute = interpolate("$[config:two;default=three]", values);
+        assertEquals("Should have been default for no match", "three", substitute);
+    }
+
+    @Test
+    public void test_interpolator_full_with_match() {
+        Map<String,String> values = new HashMap<>();
+        values.put("one", "two");
+        String substitute = interpolate("$[config:one;default=three]", values);
+        assertEquals("Wrong Replacement", "two", substitute);
+    }
+
+    @Test
+    public void test_interpolator_full_with_default() {
+        Map<String,String> values = new HashMap<>();
+        values.put("one", "two");
+        String substitute = interpolate("$[config:two;default=three]", values);
+        assertEquals("Should have been default for no match", "three", substitute);
+    }
+
+    private String interpolate(final String text, final Map<String,String> mappings) {
+        Object result = Interpolator.replace(text, (type, name, dir) -> {
+            Object answer = mappings.get(name);
+            if(answer == null) {
+                answer = dir.get("default");
+            }
+            return answer;
+        });
+        return result == null ? null : result.toString();
+    }
+
+    @Test
+    public void test_simple_one_placeholder() {
+        when(stringInterpolationProviderConfiguration.placeHolderKeyValuePairs()).thenReturn(
+            new String[] { "one=two"}
+        );
+
+        StringInterpolationProviderImpl placeholderProvider = new StringInterpolationProviderImpl();
+        placeholderProvider.activate(bundleContext, stringInterpolationProviderConfiguration);
+
+        String line = "$[config: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(bundleContext, stringInterpolationProviderConfiguration);
+
+        String line = "Here is $[config: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(bundleContext, stringInterpolationProviderConfiguration);
+
+        String line = "$[config:one] with another $[config: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(bundleContext, stringInterpolationProviderConfiguration);
+
+        String line = "Here comes $[config:one] with another $[config:three] equals $[config: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(bundleContext, 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(bundleContext, stringInterpolationProviderConfiguration);
+
+        String line = "Here comes $[config:unkown] placeholders!";
+        String substituted = placeholderProvider.substitute(line);
+        assertEquals("Wrong resolved line", "Here comes $[config: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(bundleContext, stringInterpolationProviderConfiguration);
+
+        String line = "$[config:siv.one]/";
+        String substituted = placeholderProvider.substitute(line);
+        assertEquals("Wrong resolved line", "test-value/", substituted);
+    }
+
+    @Test
+    public void test_escape_character() {
+        when(stringInterpolationProviderConfiguration.placeHolderKeyValuePairs()).thenReturn(
+            new String[] { "one=two"}
+        );
+
+        StringInterpolationProviderImpl placeholderProvider = new StringInterpolationProviderImpl();
+        placeholderProvider.activate(bundleContext, stringInterpolationProviderConfiguration);
+
+        String line = "\\$[config:one]=$[config:one]";
+        String substituted = placeholderProvider.substitute(line);
+        assertEquals("Wrong resolved line", "$[config:one]=two", substituted);
+    }
+
+    @Test
+    public void test_in_variables_substitution() {
+        when(stringInterpolationProviderConfiguration.placeHolderKeyValuePairs()).thenReturn(
+            new String[] { "one=two", "two=three"}
+        );
+
+        StringInterpolationProviderImpl placeholderProvider = new StringInterpolationProviderImpl();
+        placeholderProvider.activate(bundleContext, stringInterpolationProviderConfiguration);
+
+        String line = "$[config:$[config: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"}
+        );
+
+        StringInterpolationProviderImpl placeholderProvider = new StringInterpolationProviderImpl();
+        placeholderProvider.activate(bundleContext, stringInterpolationProviderConfiguration);
+
+        String line = "$[config:one$[config: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"}
+        );
+
+        StringInterpolationProviderImpl placeholderProvider = new StringInterpolationProviderImpl();
+        placeholderProvider.activate(bundleContext, stringInterpolationProviderConfiguration);
+
+        String line = "$[config:one]$[config:two]";
+        String substituted = placeholderProvider.substitute(line);
+        assertEquals("Wrong resolved line", "two$[config:two]", substituted);
+    }
+
+    @Test
+    public void test_bad_placeholder_value_configuration() {
+        when(stringInterpolationProviderConfiguration.placeHolderKeyValuePairs()).thenReturn(
+            new String[] { "one=two", "two="}
+        );
+
+        StringInterpolationProviderImpl placeholderProvider = new StringInterpolationProviderImpl();
+        placeholderProvider.activate(bundleContext, stringInterpolationProviderConfiguration);
+
+        String line = "$[config:one]$[config:two]";
+        String substituted = placeholderProvider.substitute(line);
+        assertEquals("Wrong resolved line", "two$[config:two]", substituted);
+    }
+
+    @Test
+    public void test_comment_in_configuration() {
+        when(stringInterpolationProviderConfiguration.placeHolderKeyValuePairs()).thenReturn(
+            new String[] { "one=two", "# Next One", "two=four"}
+        );
+
+        StringInterpolationProviderImpl placeholderProvider = new StringInterpolationProviderImpl();
+        placeholderProvider.activate(bundleContext, stringInterpolationProviderConfiguration);
+
+        String line = "$[config:one]-$[config: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"}
+        );
+
+        StringInterpolationProviderImpl placeholderProvider = new StringInterpolationProviderImpl();
+        placeholderProvider.activate(bundleContext, stringInterpolationProviderConfiguration);
+
+        String line = "$[config:one]-$[config: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 = "$[config:one]$[config: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="}
+        );
+
+        StringInterpolationProviderImpl placeholderProvider = new StringInterpolationProviderImpl();
+        placeholderProvider.activate(bundleContext, stringInterpolationProviderConfiguration);
+        when(stringInterpolationProviderConfiguration.placeHolderKeyValuePairs()).thenReturn(
+            new String[] { "one=two", "two=three"}
+        );
+        placeholderProvider.modified(bundleContext, stringInterpolationProviderConfiguration);
+
+        String line = "$[config:one]-$[config: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"}
+        );
+
+        StringInterpolationProviderImpl placeholderProvider = new StringInterpolationProviderImpl();
+        placeholderProvider.activate(bundleContext, stringInterpolationProviderConfiguration);
+
+        String line = "$[config:one]-$[config:two]";
+        String substituted = placeholderProvider.substitute(line);
+        assertEquals("Wrong resolved line", "two-four", substituted);
+        placeholderProvider.deactivate(bundleContext);
+        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..5291b5b 100644
--- a/src/test/java/org/apache/sling/resourceresolver/util/MockTestUtil.java
+++ b/src/test/java/org/apache/sling/resourceresolver/util/MockTestUtil.java
@@ -26,12 +26,15 @@
 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;
 import org.mockito.Mockito;
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.stubbing.Answer;
+import org.osgi.framework.BundleContext;
 
 import javax.servlet.http.HttpServletRequest;
 import java.lang.reflect.Field;
@@ -302,6 +305,23 @@
         }
     }
 
+    public static StringInterpolationProviderConfiguration createStringInterpolationProviderConfiguration() {
+        StringInterpolationProviderConfiguration answer = mock(StringInterpolationProviderConfiguration.class);
+        when(answer.placeHolderKeyValuePairs()).thenReturn(new String[] {});
+        return answer;
+    }
+
+    public static void setupStringInterpolationProvider(
+        StringInterpolationProvider provider, StringInterpolationProviderConfiguration configuration, final String[] placeholderValues
+    ) {
+        when(configuration.placeHolderKeyValuePairs()).thenReturn(placeholderValues);
+        BundleContext context = mock(BundleContext.class);
+        callInaccessibleMethod("activate", Void.TYPE, provider,
+            new Class[] {BundleContext.class, StringInterpolationProviderConfiguration.class},
+            new Object[] {context, configuration}
+        );
+    }
+
     public static class FieldWrapper<T> {
         private Field field;
         private Object target;