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;