Merge branch 'master' of github.com:apache/sling-org-apache-sling-resourceresolver
diff --git a/src/test/java/org/apache/sling/resourceresolver/impl/EtcMappingResourceResolverTest.java b/src/test/java/org/apache/sling/resourceresolver/impl/EtcMappingResourceResolverTest.java
new file mode 100644
index 0000000..b8f73e1
--- /dev/null
+++ b/src/test/java/org/apache/sling/resourceresolver/impl/EtcMappingResourceResolverTest.java
@@ -0,0 +1,309 @@
+/*
+ * 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;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.api.resource.observation.ResourceChange;
+import org.apache.sling.api.resource.path.Path;
+import org.apache.sling.resourceresolver.impl.mapping.MapConfigurationProvider;
+import org.apache.sling.resourceresolver.impl.mapping.MapEntries;
+import org.apache.sling.resourceresolver.impl.providers.ResourceProviderHandler;
+import org.apache.sling.resourceresolver.impl.providers.ResourceProviderStorage;
+import org.apache.sling.resourceresolver.impl.providers.ResourceProviderTracker;
+import org.apache.sling.serviceusermapping.ServiceUserMapper;
+import org.apache.sling.spi.resource.provider.ResourceProvider;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.event.EventAdmin;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import static java.util.Arrays.asList;
+import static org.apache.sling.resourceresolver.impl.MockedResourceResolverImplTest.createRPHandler;
+import static org.apache.sling.resourceresolver.impl.ResourceResolverImpl.PROP_REDIRECT_INTERNAL;
+import static org.apache.sling.resourceresolver.impl.mapping.MapEntries.PROP_REDIRECT_EXTERNAL;
+import static org.apache.sling.resourceresolver.util.MockTestUtil.ExpectedEtcMapping;
+import static org.apache.sling.resourceresolver.util.MockTestUtil.buildResource;
+import static org.apache.sling.resourceresolver.util.MockTestUtil.callInaccessibleMethod;
+import static org.apache.sling.resourceresolver.util.MockTestUtil.checkInternalResource;
+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.setInaccessibleField;
+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 are the same tests as in the EtcMappingMapEntriesTest but in this
+ * class we are actually mocking the Resource Resolver Factory and its classes
+ * and we test the mapping and resource resolution through the resource resolver
+ * rather the MapEntries.
+ */
+public class EtcMappingResourceResolverTest {
+
+    static final String PROP_REG_EXP = "sling:match";
+
+    @Mock
+    ResourceResolverFactory resourceResolverFactory;
+
+    @Mock
+    BundleContext bundleContext;
+
+    @Mock
+    Bundle bundle;
+
+    @Mock
+    EventAdmin eventAdmin;
+
+    @Mock
+    ResourceResolver resourceResolver;
+
+    @Mock
+    ResourceProvider<?> resourceProvider;
+
+    MapEntries mapEntries;
+
+    File vanityBloomFilterFile;
+
+    CommonResourceResolverFactoryImpl commonFactory;
+
+    Resource etc;
+    Resource map;
+    Resource http;
+
+    Map<String, Map<String, String>> aliasMap;
+
+    @SuppressWarnings({"unchecked"})
+    @Before
+    public void setup() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        List<MapConfigurationProvider.VanityPathConfig> configs = getVanityPathConfigs();
+        vanityBloomFilterFile = new File("target/test-classes/resourcesvanityBloomFilter.txt");
+        List<ResourceProviderHandler> handlers = asList(createRPHandler(resourceProvider, "rp1", 0, "/"));
+        ResourceProviderTracker resourceProviderTracker = mock(ResourceProviderTracker.class);
+        ResourceProviderStorage storage = new ResourceProviderStorage(handlers);
+        when(resourceProviderTracker.getResourceProviderStorage()).thenReturn(storage);
+        ResourceResolverFactoryActivator activator = new ResourceResolverFactoryActivator();
+        // These fields on the Activator a package private so we need reflection to access them
+        setInaccessibleField("resourceProviderTracker", activator, resourceProviderTracker);
+        setInaccessibleField("resourceAccessSecurityTracker", activator, new ResourceAccessSecurityTracker());
+        setInaccessibleField("bundleContext", activator, bundleContext);
+        setInaccessibleField("mapRoot", activator, "/etc/map");
+        setInaccessibleField("mapRootPrefix", activator, "/etc/map");
+        setInaccessibleField("observationPaths", activator, new Path[] {new Path("/")});
+        ServiceUserMapper serviceUserMapper = mock(ServiceUserMapper.class);
+        setInaccessibleField("serviceUserMapper", activator, serviceUserMapper);
+        commonFactory = spy(new CommonResourceResolverFactoryImpl(activator));
+        when(bundleContext.getBundle()).thenReturn(bundle);
+        when(bundleContext.getDataFile("vanityBloomFilter.txt")).thenReturn(vanityBloomFilterFile);
+        when(serviceUserMapper.getServiceUserID(any(Bundle.class),anyString())).thenReturn("mapping");
+        // Activate method is package private so we use reflection to to call it
+        callInaccessibleMethod("activate", null, commonFactory, BundleContext.class, bundleContext);
+        final Bundle usingBundle = mock(Bundle.class);
+        resourceResolverFactory = new ResourceResolverFactoryImpl(commonFactory, usingBundle, null);
+        resourceResolver = resourceResolverFactory.getAdministrativeResourceResolver(null);
+
+        etc = buildResource("/etc", null, resourceResolver, resourceProvider);
+        map = buildResource("/etc/map", etc, resourceResolver, resourceProvider);
+        http = buildResource("/etc/map/http", map, resourceResolver, resourceProvider);
+    }
+
+    List<MapConfigurationProvider.VanityPathConfig> getVanityPathConfigs() {
+        return new ArrayList<>();
+    }
+
+    /**
+     * Changes to the /etc/map in our tests are not taking effect until there is an Change Event issued
+     *
+     * ATTENTION: this method can only be issued once. After that the Resource Metadata is locked and
+     * hence updates will fail
+     *
+     * @param path Path to the resource root to be refreshed
+     * @param isExternal External flag of the ResourceChange event
+     */
+    void refreshMapEntries(String path, boolean isExternal) {
+        ((MapEntries) commonFactory.getMapEntries()).onChange(
+            asList(
+                new ResourceChange(ResourceChange.ChangeType.ADDED, path, isExternal)
+            )
+        );
+    }
+
+    @Test
+    public void root_node_to_content_mapping() throws Exception {
+        buildResource(http.getPath() + "/localhost.8080", http, resourceResolver, resourceProvider, PROP_REDIRECT_EXTERNAL, "/content/simple-node");
+        // This updates the map entries so that the newly added resources are added.
+        // ATTENTION: only call this after all etc-mapping resources are defined as this lock their Resource Meta Data and prevents a re-update
+        refreshMapEntries("/etc/map", true);
+
+        ExpectedEtcMapping expectedEtcMapping = new ExpectedEtcMapping("^http/localhost.8080/", "/content/simple-node/");
+        expectedEtcMapping.assertEtcMap("Etc Mapping for root node to content", commonFactory.getMapEntries().getResolveMaps());
+
+        HttpServletRequest request = createRequestFromUrl("http://localhost:8080/");
+        Resource resolvedResource = resourceResolver.resolve(request, "/");
+        checkRedirectResource(resolvedResource, "/content/simple-node/", 302);
+    }
+
+    @Test
+    public void match_to_content_mapping() throws Exception {
+        buildResource("test-node", http, resourceResolver, resourceProvider,
+            PROP_REG_EXP, "localhost.8080/",
+            PROP_REDIRECT_EXTERNAL, "/content/simple-match/"
+        );
+        refreshMapEntries("/etc/map", true);
+
+        ExpectedEtcMapping expectedEtcMapping = new ExpectedEtcMapping("^http/localhost.8080/", "/content/simple-match/");
+        expectedEtcMapping.assertEtcMap("Etc Mapping for match to content", commonFactory.getMapEntries().getResolveMaps());
+
+        HttpServletRequest request = createRequestFromUrl("http://localhost:8080/");
+        Resource resolvedResource = resourceResolver.resolve(request, "/");
+        checkRedirectResource(resolvedResource, "/content/simple-match/", 302);
+    }
+
+    // The following tests are based on the example from the https://sling.apache.org/documentation/the-sling-engine/mappings-for-resource-resolution.html page
+
+    @Test
+    public void internal_to_external_node_mapping() throws Exception {
+        buildResource("example.com.80", http, resourceResolver, resourceProvider, PROP_REDIRECT_EXTERNAL, "http://www.example.com/");
+        refreshMapEntries("/etc/map", true);
+
+        ExpectedEtcMapping expectedEtcMapping = new ExpectedEtcMapping("^http/example.com.80/", "http://www.example.com/");
+        expectedEtcMapping.assertEtcMap("Etc Mapping for internal to external based on node", commonFactory.getMapEntries().getResolveMaps());
+
+        HttpServletRequest request = createRequestFromUrl("http://example.com/");
+        Resource resolvedResource = resourceResolver.resolve(request, "/");
+        checkRedirectResource(resolvedResource, "http://www.example.com/", 302);
+    }
+
+    @Test
+    public void internal_root_to_content_node_mapping() throws Exception {
+        buildResource("/example", null, resourceResolver, resourceProvider);
+
+        buildResource("www.example.com.80", http, resourceResolver, resourceProvider, PROP_REDIRECT_INTERNAL, "/example");
+        refreshMapEntries("/etc/map", true);
+
+        ExpectedEtcMapping expectedEtcMapping = new ExpectedEtcMapping().addEtcMapEntry("^http/www.example.com.80/", true, "/example/");
+        expectedEtcMapping.assertEtcMap("Etc Mapping for internal root to content", commonFactory.getMapEntries().getResolveMaps());
+
+        HttpServletRequest request = createRequestFromUrl("http://www.example.com:80/");
+        Resource resolvedResource = resourceResolver.resolve(request, "/");
+        checkInternalResource(resolvedResource, "/example");
+    }
+
+    @Test
+    public void host_redirect_match_mapping() throws Exception {
+        buildResource("any_example.com.80", http, resourceResolver, resourceProvider,
+            PROP_REG_EXP, ".+\\.example\\.com\\.80",
+            PROP_REDIRECT_EXTERNAL, "http://www.example.com/"
+        );
+        refreshMapEntries("/etc/map", true);
+
+        ExpectedEtcMapping expectedEtcMapping = new ExpectedEtcMapping().addEtcMapEntry("^http/.+\\.example\\.com\\.80", false, "http://www.example.com/");
+        expectedEtcMapping.assertEtcMap("Etc Mapping for host redirect match mapping", commonFactory.getMapEntries().getResolveMaps());
+
+        HttpServletRequest request = createRequestFromUrl("http://www.example.com");
+        Resource resolvedResource = resourceResolver.resolve(request, "/");
+        checkRedirectResource(resolvedResource, "http://www.example.com//", 302);
+    }
+
+    @Test
+    public void nested_internal_mixed_mapping() throws Exception {
+        Resource localhost = buildResource("localhost_any", http, resourceResolver, resourceProvider,
+            PROP_REG_EXP, "localhost\\.\\d*",
+            PROP_REDIRECT_INTERNAL, "/content"
+        );
+        buildResource("cgi-bin", localhost, resourceResolver, resourceProvider,PROP_REDIRECT_INTERNAL, "/scripts");
+        buildResource("gateway", localhost, resourceResolver, resourceProvider,PROP_REDIRECT_INTERNAL, "http://gbiv.com");
+        buildResource("(stories)", localhost, resourceResolver, resourceProvider,PROP_REDIRECT_INTERNAL, "/anecdotes/$1");
+
+        refreshMapEntries("/etc/map", true);
+
+        ExpectedEtcMapping expectedEtcMapping = new ExpectedEtcMapping()
+            .addEtcMapEntry("^http/localhost\\.\\d*", true, "/content")
+            .addEtcMapEntry("^http/localhost\\.\\d*/cgi-bin/", true, "/scripts/")
+            .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", commonFactory.getMapEntries().getResolveMaps());
+
+        buildResource("/content", null, resourceResolver, resourceProvider);
+        Resource scripts = buildResource("/scripts", null, resourceResolver, resourceProvider);
+        Resource scriptsChild = buildResource("/scripts/child", scripts, resourceResolver, resourceProvider);
+        Resource anecdotes = buildResource("/anecdotes", null, resourceResolver, resourceProvider);
+        Resource stories = buildResource("/anecdotes/stories", anecdotes, resourceResolver, resourceProvider);
+
+        HttpServletRequest request = createRequestFromUrl("http://localhost:1234/");
+        Resource resolvedResource = resourceResolver.resolve(request, "/");
+        checkInternalResource(resolvedResource, "/content");
+
+        resolvedResource = resourceResolver.resolve(request, "/cgi-bin/");
+        checkInternalResource(resolvedResource, "/scripts");
+        resolvedResource = resourceResolver.resolve(request, "/cgi-bin/child/");
+        checkInternalResource(resolvedResource, "/scripts/child");
+//AS TODO: Does not redirect -> investigate later
+//        resolvedResource = resourceResolver.resolve(request, "/gateway/");
+//        checkRedirectResource(resolvedResource, "http://gbiv.com/", 302);
+        resolvedResource = resourceResolver.resolve(request, "/stories/");
+        checkInternalResource(resolvedResource, "/anecdotes/stories");
+    }
+
+    /**
+     * 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
+     * Resolver factory.
+     * Either the Etc Mapping discovers this condition and stops it or at least ignores mapping for Composum to allow
+     * the /etc/map to be edited.
+     */
+    @Test
+    public void endless_circular_mapping() throws Exception {
+        buildResource(http.getPath() + "/localhost.8080", http, resourceResolver, resourceProvider, PROP_REDIRECT_EXTERNAL, "/content");
+        refreshMapEntries("/etc/map", true);
+
+        ExpectedEtcMapping expectedEtcMapping = new ExpectedEtcMapping("^http/localhost.8080/", "/content/");
+        expectedEtcMapping.assertEtcMap("Etc Mapping for root node to content", commonFactory.getMapEntries().getResolveMaps());
+
+        buildResource("/content/test", null, resourceResolver, resourceProvider);
+        buildResource("/content/content/test", null, resourceResolver, resourceProvider);
+        buildResource("/content/content/content/test", null, resourceResolver, resourceProvider);
+
+        HttpServletRequest request = createRequestFromUrl("http://localhost:8080/");
+        Resource resolvedResource = resourceResolver.resolve(request, "/test.html");
+        checkRedirectResource(resolvedResource, "/content/test.html", 302);
+
+        resolvedResource = resourceResolver.resolve(request, "/content/test.html");
+        checkRedirectResource(resolvedResource, "/content/content/test.html", 302);
+
+        resolvedResource = resourceResolver.resolve(request, "/content/content/test.html");
+        checkRedirectResource(resolvedResource, "/content/content/content/test.html", 302);
+    }
+}
diff --git a/src/test/java/org/apache/sling/resourceresolver/impl/SimpleValueMapImpl.java b/src/test/java/org/apache/sling/resourceresolver/impl/SimpleValueMapImpl.java
index f1ed9c1..ef71aa7 100644
--- a/src/test/java/org/apache/sling/resourceresolver/impl/SimpleValueMapImpl.java
+++ b/src/test/java/org/apache/sling/resourceresolver/impl/SimpleValueMapImpl.java
@@ -84,7 +84,11 @@
     public <T> T get(String name, Class<T> type) {
         Object o = delegate.get(name);
         if ( type.equals(String[].class) && ! ( o instanceof String[])) {
-            o = new String[] { String.valueOf(o) };
+            // According to ValueMap if the value cannot be converted it should return null
+            // If 'o' is null this would return String[] {null} instead so we do not convert it here
+            if(o != null) {
+                o = new String[]{String.valueOf(o)};
+            }
         }
         return (T) o;
     }
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
new file mode 100644
index 0000000..6a5f9e2
--- /dev/null
+++ b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/AbstractMappingMapEntriesTest.java
@@ -0,0 +1,227 @@
+/*
+ * 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.apache.sling.api.resource.ResourceMetadata;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.api.resource.path.Path;
+import org.apache.sling.api.wrappers.ValueMapDecorator;
+import org.junit.After;
+import org.junit.Before;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.event.EventAdmin;
+
+import java.io.File;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.Semaphore;
+
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyMap;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.withSettings;
+
+/**
+ * These are tests that are testing the Sling Interpolation Feature (SLING-7768)
+ * on the MapEntries level
+ */
+public abstract class AbstractMappingMapEntriesTest {
+    static final String PROP_REG_EXP = "sling:match";
+
+    @Mock
+    MapConfigurationProvider resourceResolverFactory;
+
+    @Mock
+    BundleContext bundleContext;
+
+    @Mock
+    Bundle bundle;
+
+    @Mock
+    EventAdmin eventAdmin;
+
+    @Mock
+    ResourceResolver resourceResolver;
+
+    MapEntries mapEntries;
+
+    File vanityBloomFilterFile;
+
+    Resource map;
+    Resource http;
+
+    Map<String, Map<String, String>> aliasMap;
+
+    @SuppressWarnings({"unchecked"})
+    @Before
+    public void setup() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        List<MapConfigurationProvider.VanityPathConfig> configs = getVanityPathConfigs();
+        vanityBloomFilterFile = new File("target/test-classes/resourcesvanityBloomFilter.txt");
+        when(bundle.getSymbolicName()).thenReturn("TESTBUNDLE");
+        when(bundleContext.getBundle()).thenReturn(bundle);
+        when(bundleContext.getDataFile("vanityBloomFilter.txt")).thenReturn(vanityBloomFilterFile);
+        when(resourceResolverFactory.getServiceResourceResolver(any(Map.class))).thenReturn(resourceResolver);
+        when(resourceResolverFactory.isVanityPathEnabled()).thenReturn(true);
+        when(resourceResolverFactory.getVanityPathConfig()).thenReturn(configs);
+        when(resourceResolverFactory.isOptimizeAliasResolutionEnabled()).thenReturn(true);
+        when(resourceResolverFactory.getObservationPaths()).thenReturn(new Path[] {new Path("/")});
+        when(resourceResolverFactory.getMapRoot()).thenReturn(MapEntries.DEFAULT_MAP_ROOT);
+        when(resourceResolverFactory.getMaxCachedVanityPathEntries()).thenReturn(-1L);
+        when(resourceResolverFactory.isMaxCachedVanityPathEntriesStartup()).thenReturn(true);
+        when(resourceResolver.findResources(anyString(), eq("sql"))).thenReturn(
+            Collections.<Resource> emptySet().iterator());
+
+        map = setupEtcMapResource("/etc", "map");
+        http = setupEtcMapResource("http", map);
+
+        mapEntries = new MapEntries(resourceResolverFactory, bundleContext, eventAdmin);
+
+        final Field aliasMapField = MapEntries.class.getDeclaredField("aliasMap");
+        aliasMapField.setAccessible(true);
+        this.aliasMap = ( Map<String, Map<String, String>>) aliasMapField.get(mapEntries);
+    }
+
+    List<MapConfigurationProvider.VanityPathConfig> getVanityPathConfigs() {
+        return new ArrayList<>();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        vanityBloomFilterFile.delete();
+    }
+
+
+    // -------------------------- private methods ----------
+
+    ValueMap buildValueMap(Object... string) {
+        final Map<String, Object> data = new HashMap<>();
+        for (int i = 0; i < string.length; i = i + 2) {
+            data.put((String) string[i], string[i+1]);
+        }
+        return new ValueMapDecorator(data);
+    }
+
+    Resource getVanityPathResource(final String path) {
+        Resource rsrc = mock(Resource.class);
+        when(rsrc.getPath()).thenReturn(path);
+        when(rsrc.getName()).thenReturn(ResourceUtil.getName(path));
+        when(rsrc.getValueMap()).thenReturn(buildValueMap("sling:vanityPath", "/vanity" + path));
+        return rsrc;
+    }
+
+    Resource setupEtcMapResource(String parentPath, String name, String...valueMapPairs) {
+        return setupEtcMapResource0(parentPath, name, null, valueMapPairs);
+    }
+    Resource setupEtcMapResource(String name, Resource parent, String...valueMapPairs) {
+        return setupEtcMapResource0(null, name, parent, valueMapPairs);
+    }
+    private Resource setupEtcMapResource0(String parentPath, String name, Resource parent, String...valueMapPairs) {
+        Resource resource = mock(Resource.class, withSettings().name(name).extraInterfaces(ResourceDecorator.class));
+        String path = (parent == null ? parentPath : parent.getPath()) + "/" + name;
+        when(resource.getPath()).thenReturn(path);
+        when(resource.getName()).thenReturn(name);
+        ValueMap valueMap = buildValueMap(valueMapPairs);
+        when(resource.getValueMap()).thenReturn(valueMap);
+        when(resource.adaptTo(ValueMap.class)).thenReturn(valueMap);
+        when(resourceResolver.getResource(resource.getPath())).thenReturn(resource);
+        if(parent != null) {
+            List<Resource> childList = ((ResourceDecorator) parent).getChildrenList();
+            childList.add(resource);
+        }
+        final List<Resource> childrenList = new ArrayList<>();
+        when(((ResourceDecorator) resource).getChildrenList()).thenReturn(childrenList);
+        // Delay the children list iterator to make sure all children are added beforehand
+        // Iterators have a modCount that is set when created. Any changes to the underlying list will
+        // change that modCount and the usage of the iterator will fail due to Concurrent Modification Exception
+        when(resource.listChildren()).thenAnswer(new Answer<Iterator<Resource>>() {
+            @Override
+            public Iterator<Resource> answer(InvocationOnMock invocation) throws Throwable {
+                return childrenList.iterator();
+            }
+        });
+        ResourceMetadata resourceMetadata = mock(ResourceMetadata.class);
+        when(resource.getResourceMetadata()).thenReturn(resourceMetadata);
+        doNothing().when(resourceMetadata).setResolutionPath(anyString());
+        doNothing().when(resourceMetadata).setParameterMap(anyMap());
+
+        return resource;
+    }
+
+    DataFuture createDataFuture(ExecutorService pool, final MapEntries mapEntries) {
+
+        Future<Iterator<?>> future = pool.submit(new Callable<Iterator<?>>() {
+            @Override
+            public Iterator<MapEntry> call() throws Exception {
+                return mapEntries.getResolveMapsIterator("http/localhost.8080/target/justVanityPath");
+            }
+        });
+        return new DataFuture(future);
+    }
+
+    void simulateSomewhatSlowSessionOperation(final Semaphore sessionLock) throws InterruptedException {
+        if (!sessionLock.tryAcquire()) {
+            fail("concurrent session access detected");
+        }
+        try{
+            Thread.sleep(1);
+        } finally {
+            sessionLock.release();
+        }
+    }
+
+    /**
+     * Iterator to piggyback the list of Resources onto a Resource Mock
+     * so that we can add children to them and create the iterators after
+     * everything is setup
+     */
+    static interface ResourceDecorator {
+        public List<Resource> getChildrenList();
+    }
+
+    static class DataFuture {
+        public Future<Iterator<?>> future;
+
+        public DataFuture(Future<Iterator<?>> future) {
+            super();
+            this.future = future;
+        }
+    }
+
+}
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
new file mode 100644
index 0000000..42bce46
--- /dev/null
+++ b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/EtcMappingMapEntriesTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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.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 static org.apache.sling.resourceresolver.util.MockTestUtil.ExpectedEtcMapping;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+/**
+ * These tests are for the /etc/map setup of the Map Entries when
+ * an /etc/map is present.
+ */
+public class EtcMappingMapEntriesTest extends AbstractMappingMapEntriesTest {
+
+    @Test
+    public void root_node_to_content_mapping() throws Exception {
+        setupEtcMapResource("localhost.8080", http,PROP_REDIRECT_EXTERNAL, "/content/simple-node");
+
+        mapEntries.doInit();
+        ExpectedEtcMapping expectedEtcMapping = new ExpectedEtcMapping("^http/localhost.8080/", "/content/simple-node/");
+        expectedEtcMapping.assertEtcMap("Etc Mapping for simple node", mapEntries.getResolveMaps());
+    }
+
+    @Test
+    public void match_to_content_mapping() throws Exception {
+        setupEtcMapResource("test-node", http,
+            PROP_REG_EXP, "localhost.8080/",
+            PROP_REDIRECT_EXTERNAL, "/content/simple-match/"
+        );
+
+        mapEntries.doInit();
+        ExpectedEtcMapping expectedEtcMapping = new ExpectedEtcMapping("^http/localhost.8080/", "/content/simple-match/");
+        expectedEtcMapping.assertEtcMap("Etc Mapping for simple match", mapEntries.getResolveMaps());
+    }
+
+    // The following tests are based on the example from the https://sling.apache.org/documentation/the-sling-engine/mappings-for-resource-resolution.html page
+
+    @Test
+    public void internal_to_external_node_mapping() throws Exception {
+        setupEtcMapResource("example.com.80", http,PROP_REDIRECT_EXTERNAL, "http://www.example.com/");
+
+        mapEntries.doInit();
+        ExpectedEtcMapping expectedEtcMapping = new ExpectedEtcMapping("^http/example.com.80/", "http://www.example.com/");
+        expectedEtcMapping.assertEtcMap("Etc Mapping for internal to external based on node", mapEntries.getResolveMaps());
+    }
+
+    @Test
+    public void internal_root_to_content_node_mapping() throws Exception {
+        setupEtcMapResource("www.example.com.80", http,PROP_REDIRECT_INTERNAL, "/example");
+
+        mapEntries.doInit();
+        ExpectedEtcMapping expectedEtcMapping = new ExpectedEtcMapping().addEtcMapEntry("^http/www.example.com.80/", true, "/example/");
+        expectedEtcMapping.assertEtcMap("Etc Mapping for internal root to content", mapEntries.getResolveMaps());
+    }
+
+    @Test
+    public void host_redirect_match_mapping() throws Exception {
+        setupEtcMapResource("any_example.com.80", http,
+            PROP_REG_EXP, ".+\\.example\\.com\\.80",
+            PROP_REDIRECT_EXTERNAL, "http://www.example.com/"
+        );
+
+        mapEntries.doInit();
+        ExpectedEtcMapping expectedEtcMapping = new ExpectedEtcMapping().addEtcMapEntry("^http/.+\\.example\\.com\\.80", false, "http://www.example.com/");
+        expectedEtcMapping.assertEtcMap("Etc Mapping for host redirect match mapping", mapEntries.getResolveMaps());
+    }
+
+    @Test
+    public void nested_internal_mixed_mapping() throws Exception {
+        Resource localhost = setupEtcMapResource("localhost_any", http,
+            PROP_REG_EXP, "localhost\\.\\d*",
+            PROP_REDIRECT_INTERNAL, "/content"
+        );
+        setupEtcMapResource("cgi-bin", localhost, PROP_REDIRECT_INTERNAL, "/scripts");
+        setupEtcMapResource("gateway", localhost, PROP_REDIRECT_INTERNAL, "http://gbiv.com");
+        setupEtcMapResource("(stories)", localhost, PROP_REDIRECT_INTERNAL, "/anecdotes/$1");
+
+        mapEntries.doInit();
+        ExpectedEtcMapping expectedEtcMapping = new ExpectedEtcMapping()
+            .addEtcMapEntry("^http/localhost\\.\\d*", true, "/content")
+            .addEtcMapEntry("^http/localhost\\.\\d*/cgi-bin/", true, "/scripts/")
+            .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());
+    }
+}
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 8864fb8..5758e49 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
@@ -63,6 +63,7 @@
 import org.apache.sling.api.wrappers.ValueMapDecorator;
 import org.apache.sling.resourceresolver.impl.ResourceResolverImpl;
 import org.apache.sling.resourceresolver.impl.mapping.MapConfigurationProvider.VanityPathConfig;
+import org.apache.sling.resourceresolver.util.MockTestUtil;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -75,7 +76,7 @@
 import org.osgi.framework.BundleContext;
 import org.osgi.service.event.EventAdmin;
 
-public class MapEntriesTest {
+public class MapEntriesTest extends AbstractMappingMapEntriesTest {
 
     private MapEntries mapEntries;
 
@@ -371,22 +372,6 @@
         assertTrue( mapEntries.getResolveMaps().isEmpty());
     }
 
-    private ValueMap buildValueMap(Object... string) {
-        final Map<String, Object> data = new HashMap<>();
-        for (int i = 0; i < string.length; i = i + 2) {
-            data.put((String) string[i], string[i+1]);
-        }
-        return new ValueMapDecorator(data);
-    }
-
-    private Resource getVanityPathResource(final String path) {
-        Resource rsrc = mock(Resource.class);
-        when(rsrc.getPath()).thenReturn(path);
-        when(rsrc.getName()).thenReturn(ResourceUtil.getName(path));
-        when(rsrc.getValueMap()).thenReturn(buildValueMap("sling:vanityPath", "/vanity" + path));
-        return rsrc;
-    }
-
     @Test
     public void test_vanity_path_registration_include_exclude() throws IOException {
         final String[] validPaths = {"/libs/somewhere", "/libs/a/b", "/foo/a", "/baa/a"};
@@ -2096,39 +2081,4 @@
             }
         }
     }
-
-    // -------------------------- private methods ----------
-    private DataFuture createDataFuture(ExecutorService pool, final MapEntries mapEntries) {
-
-        Future<Iterator<?>> future = pool.submit(new Callable<Iterator<?>>() {
-            @Override
-            public Iterator<MapEntry> call() throws Exception {
-                return mapEntries.getResolveMapsIterator("http/localhost.8080/target/justVanityPath");
-            }
-        });
-        return new DataFuture(future);
-    }
-
-    private void simulateSomewhatSlowSessionOperation(final Semaphore sessionLock) throws InterruptedException {
-        if (!sessionLock.tryAcquire()) {
-            fail("concurrent session access detected");
-        }
-        try{
-            Thread.sleep(1);
-        } finally {
-            sessionLock.release();
-        }
-    }
-
-    // -------------------------- inner classes ------------
-
-    private static class DataFuture {
-        public Future<Iterator<?>> future;
-
-        public DataFuture(Future<Iterator<?>> future) {
-            super();
-            this.future = future;
-        }
-    }
-
 }
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/resourceresolver/util/MockTestUtil.java b/src/test/java/org/apache/sling/resourceresolver/util/MockTestUtil.java
new file mode 100644
index 0000000..1480397
--- /dev/null
+++ b/src/test/java/org/apache/sling/resourceresolver/util/MockTestUtil.java
@@ -0,0 +1,407 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.sling.resourceresolver.util;
+
+import junit.framework.TestCase;
+import org.apache.sling.api.resource.NonExistingResource;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceMetadata;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.api.wrappers.ValueMapDecorator;
+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.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 javax.servlet.http.HttpServletRequest;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+
+import static java.util.Arrays.asList;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.withSettings;
+
+public class MockTestUtil {
+
+    static final String PROP_SLING_TARGET = "sling:target";
+    static final String PROP_SLING_STATUS = "sling:status";
+
+    public static void checkRedirectResource(Resource redirect, String target, int status) {
+        assertThat("Not a Redirect Resource", redirect, instanceOf(RedirectResource.class));
+        RedirectResource redirectResource = (RedirectResource) redirect;
+        ValueMap values = redirectResource.adaptTo(ValueMap.class);
+        assertEquals("Redirect Target is wrong", target, values.get(PROP_SLING_TARGET, String.class));
+        assertEquals("Redirect Status is wrong", new Integer(status), values.get(PROP_SLING_STATUS, Integer.class));
+    }
+
+    public static void checkNonExistingResource(Resource redirect, String path) {
+        assertThat("Not a Non Existing Resource", redirect, instanceOf(NonExistingResource.class));
+        NonExistingResource nonExistingResource = (NonExistingResource) redirect;
+        if(path != null) {
+            assertEquals("Wrong Path for Non Existing Resource", path, nonExistingResource.getPath());
+        }
+    }
+
+    public static void checkInternalResource(Resource internal, String path) {
+        assertEquals("Wrong Path for Resource", path, internal.getPath());
+    }
+
+    /**
+     * Extract the name from a resource path
+     *
+     * @param fullPath Full / Aboslute path to the resource
+     * @return Name of the resource
+     */
+    public static String getResourceName(String fullPath) {
+        int n = fullPath.lastIndexOf("/");
+        return fullPath.substring(n + 1);
+    }
+
+    /**
+     * Creates a Mock Http Servlet Request
+     * @param url Absolute URL to be used to get the method, host and port
+     * @return Http Servlet Request if the url is valid otherwise null
+     */
+    public static HttpServletRequest createRequestFromUrl(String url) {
+        int index = url.indexOf("://");
+        if(index > 0) {
+            String method = url.substring(0, index);
+            int port = 80;
+            int index2 = url.indexOf(":", index + 3);
+            int index3 = url.indexOf("/", index2 > index ? index2 : index + 3);
+            String host = "";
+            if (index2 > 0) {
+                port = new Integer(url.substring(index2 + 1, index3));
+                host = url.substring(index + 3, index2);
+            } else {
+                if(index3 > 0) {
+                    host = url.substring(index + 3, index3);
+                } else {
+                    host = url.substring(index + 3);
+                }
+            }
+            HttpServletRequest request = mock(HttpServletRequest.class);
+            when(request.getScheme()).thenReturn(method);
+            when(request.getServerName()).thenReturn(host);
+            when(request.getServerPort()).thenReturn(port);
+            return request;
+        }
+        return null;
+    }
+
+    /**
+     * Build a resource with path, parent, provider and resource resolver.
+     *
+     * @param fullPath         Full Path of the Resource
+     * @param parent           Parent of this resource but it can be null
+     * @param resourceResolver Resource Resolver of this resource
+     * @param provider         Resource Provider Instance
+     * @param properties       Key / Value pair for resource properties (the number of strings must be even)
+     * @return Mock Resource able to handle addition of children later on
+     */
+    @SuppressWarnings("unchecked")
+    public static Resource buildResource(String fullPath, Resource parent, ResourceResolver resourceResolver, ResourceProvider<?> provider, String... properties) {
+        if (properties != null && properties.length % 2 != 0) {
+            throw new IllegalArgumentException("List of Resource Properties must be an even number: " + asList(properties));
+        }
+        Resource resource = mock(Resource.class, withSettings().name(getResourceName(fullPath)).extraInterfaces(ResourceChildrenAccessor.class));
+        when(resource.getName()).thenReturn(getResourceName(fullPath));
+        when(resource.getPath()).thenReturn(fullPath);
+        ResourceMetadata resourceMetadata = new ResourceMetadata();
+        when(resource.getResourceMetadata()).thenReturn(resourceMetadata);
+        when(resource.getResourceResolver()).thenReturn(resourceResolver);
+
+        if (parent != null) {
+            List<Resource> childList = ((ResourceChildrenAccessor) parent).getChildrenList();
+            childList.add(resource);
+        }
+        final List<Resource> childrenList = new ArrayList<>();
+        when(((ResourceChildrenAccessor) resource).getChildrenList()).thenReturn(childrenList);
+        // Delay the children list iterator to make sure all children are added beforehand
+        // Iterators have a modCount that is set when created. Any changes to the underlying list will
+        // change that modCount and the usage of the iterator will fail due to Concurrent Modification Exception
+        when(resource.listChildren()).thenAnswer(new Answer<Iterator<Resource>>() {
+            @Override
+            public Iterator<Resource> answer(InvocationOnMock invocation) throws Throwable {
+                return childrenList.iterator();
+            }
+        });
+
+        // register the resource with the provider
+        if (provider != null) {
+            when(provider.listChildren(Mockito.any(ResolveContext.class), Mockito.eq(resource))).thenAnswer(new Answer<Iterator<Resource>>() {
+                @Override
+                public Iterator<Resource> answer(InvocationOnMock invocation) throws Throwable {
+                    return childrenList.iterator();
+                }
+            });
+            when(provider.getResource(Mockito.any(ResolveContext.class), Mockito.eq(fullPath), Mockito.any(ResourceContext.class), Mockito.any(Resource.class))).thenReturn(resource);
+        }
+        if (properties != null) {
+            ValueMap vm = new SimpleValueMapImpl();
+            for (int i = 0; i < properties.length; i += 2) {
+                resourceMetadata.put(properties[i], properties[i + 1]);
+                vm.put(properties[i], properties[i + 1]);
+            }
+            when(resource.getValueMap()).thenReturn(vm);
+            when(resource.adaptTo(Mockito.eq(ValueMap.class))).thenReturn(vm);
+        } else {
+            when(resource.getValueMap()).thenReturn(ValueMapDecorator.EMPTY);
+            when(resource.adaptTo(Mockito.eq(ValueMap.class))).thenReturn(ValueMapDecorator.EMPTY);
+        }
+
+        return resource;
+    }
+
+    /**
+     * Calls a private method that has no parameter like a getter
+     *
+     * @param methodName Name of the method
+     * @param target Target instance
+     * @return Object that is returned from the method call
+     *
+     * @throws UnsupportedOperationException If the call failed because it method is not found, has no access or invocation failed
+     */
+    public static <T> T callInaccessibleMethod(String methodName, Class<T> returnType, Object target) {
+        return callInaccessibleMethod(methodName, returnType, target, new Class[] {}, new Object[] {});
+    }
+
+    /**
+     * Calls a private method that has one parameter like a setter method
+     *
+     * @param methodName Name of the method
+     * @param target Target instance
+     * @param paramsType Parameter Type which cannot be null
+     * @param param Parameter Value
+     * @return Object that is returned from the method call
+     *
+     * @throws UnsupportedOperationException If the call failed because it method is not found, has no access or invocation failed
+     */
+    public static <T> T callInaccessibleMethod(String methodName, Class<T> returnType, Object target, Class paramsType, Object param) {
+        return callInaccessibleMethod(methodName, returnType, target, new Class[] {paramsType}, new Object[] {param});
+    }
+
+    /**
+     * Calls a private method that has none or one parameter like a setter method
+     *
+     * ATTENTION: If parameter types of values is null then both are set to null. Also the length of the arrays must
+     * be the same
+     *
+     * @param methodName Name of the method
+     * @param target Target instance
+     * @param parameterTypes Parameter Types which must not be null
+     * @param parameters Parameter Values which must not be null
+     * @return Object that is returned from the method call
+     *
+     * @throws IllegalArgumentException If the parameter types and values do not match
+     * @throws UnsupportedOperationException If the call failed because it method is not found, has no access or invocation failed
+     */
+    public static <T> T callInaccessibleMethod(String methodName, Class<T> returnType, Object target, Class[] parameterTypes, Object[] parameters) {
+        if(parameterTypes != null && parameters != null) {
+            if(parameters.length != parameterTypes.length) { throw new IllegalArgumentException("Number of Parameter Types and Values were not the same"); }
+        } else {
+            throw new IllegalArgumentException("Parameter Type and Value Array cannot be null");
+        }
+        try {
+            return getInaccessibleMethod(methodName, returnType, target, parameterTypes).call(parameters);
+        } catch (IllegalAccessException e) {
+            throw new UnsupportedOperationException("Failed to access method: " + methodName, e);
+        } catch (InvocationTargetException e) {
+            throw new UnsupportedOperationException("Failed to invoke method: " + methodName, e);
+        }
+    }
+
+    public static <T> MethodWrapper<T> getInaccessibleMethod(String methodName, Class<T> returnType, Object target, Class...parameterTypes) {
+        return new MethodWrapper(methodName, returnType, target, parameterTypes);
+    }
+
+    public static class MethodWrapper<T> {
+        private Method method;
+        private Object target;
+
+        public MethodWrapper(String methodName, Class<T> returnType, Object target, Class[] parameterTypes) {
+            try {
+                this.method = target.getClass().getDeclaredMethod(methodName, parameterTypes);
+                this.method.setAccessible(true);
+                this.target = target;
+                if(returnType == null && !this.method.getReturnType().equals(Void.TYPE)) {
+                    throw new IllegalArgumentException("Return Type is null but method does not return Void but: " + this.method.getReturnType());
+                }
+                if(returnType != null && !returnType.isAssignableFrom(this.method.getReturnType())) {
+                    throw new IllegalArgumentException("Return Type is not assignable to: " + returnType + ", it returns this: " + this.method.getReturnType());
+                }
+            } catch (NoSuchMethodException e) {
+                throw new UnsupportedOperationException("Failed to find method: " + methodName, e);
+            }
+        }
+
+        public T call(Object...parameters) throws InvocationTargetException, IllegalAccessException {
+            return (T) method.invoke(target, parameters);
+        }
+    }
+
+    /**
+     * Sets the value of a private field
+     *
+     * @param fieldName Name of the field to be set
+     * @param target Target instance
+     * @param fieldValue Value to be set
+     *
+     * @throws UnsupportedOperationException If the call failed because it field is not found or has no access
+     */
+    public static void setInaccessibleField(String fieldName, Object target, Object fieldValue) throws NoSuchMethodException {
+        try {
+            getInaccessibleFieldWrapper(fieldName, target, Object.class).set(fieldValue);
+        } catch (IllegalAccessException e) {
+            throw new UnsupportedOperationException("Failed to access field: " + fieldName, e);
+        }
+    }
+
+    public static <T> T getInaccessibleField(String fieldName, Object target, Class<T> type) {
+        try {
+            return getInaccessibleFieldWrapper(fieldName, target, type).get();
+        } catch (IllegalAccessException e) {
+            throw new UnsupportedOperationException("Failed to access field: " + fieldName, e);
+        }
+    }
+
+    public static <T> FieldWrapper<T> getInaccessibleFieldWrapper(String fieldName, Object target, Class<T> type) {
+        try {
+            return new FieldWrapper(fieldName, target, type);
+        } catch (NoSuchFieldException e) {
+            throw new UnsupportedOperationException("Failed to find field: " + fieldName, e);
+        }
+    }
+
+    public static class FieldWrapper<T> {
+        private Field field;
+        private Object target;
+
+        public FieldWrapper(String fieldName, Object target, Class<T> type) throws NoSuchFieldException {
+            this.field = target.getClass().getDeclaredField(fieldName);
+            this.field.setAccessible(true);
+            this.target = target;
+        }
+
+        public void set(T parameter) throws IllegalAccessException {
+            field.set(target, parameter);
+        }
+
+        public T get() throws IllegalAccessException {
+            return (T) field.get(target);
+        }
+    }
+
+    /**
+     * Iterator to piggyback the list of Resources onto a Resource Mock
+     * so that we can add children to them and create the iterators after
+     * everything is setup
+     */
+    static interface ResourceChildrenAccessor {
+        public List<Resource> getChildrenList();
+    }
+
+    /**
+     * Defines the Result of the Etc Mapping for easy testing
+     */
+    public static class ExpectedEtcMapping {
+        List<ExpectedEtcMapEntry> expectedEtcMapEntries = new ArrayList<>();
+
+        public ExpectedEtcMapping() {}
+
+        public ExpectedEtcMapping(String...expectedMapping) {
+            if(expectedMapping.length % 2 != 0) {
+                throw new IllegalArgumentException("Expect an even number of strings with pattern / redirect");
+            }
+            int size = expectedMapping.length / 2;
+            for(int i = 0; i < size; i++ ) {
+                expectedEtcMapEntries.add(new ExpectedEtcMapEntry(expectedMapping[2 * i], expectedMapping[2 * i + 1]));
+            }
+        }
+
+        public ExpectedEtcMapping addEtcMapEntry(String pattern, String redirect) {
+            addEtcMapEntry(pattern, false, redirect);
+            return this;
+        }
+        public ExpectedEtcMapping addEtcMapEntry(String pattern, boolean internal, String redirect) {
+            expectedEtcMapEntries.add(new ExpectedEtcMapEntry(pattern, internal, redirect));
+            return this;
+        }
+
+        public void assertEtcMap(String title, List<MapEntry> mapEntries) {
+            assertEquals("Wrong Number of Mappings for: " + title, expectedEtcMapEntries.size(), mapEntries.size());
+            ArrayList<MapEntry> actual = new ArrayList<>(mapEntries);
+            ArrayList<ExpectedEtcMapEntry> expected = new ArrayList<>(expectedEtcMapEntries);
+            for(MapEntry actualMapEntry: actual) {
+                ExpectedEtcMapEntry expectedFound = null;
+                for(ExpectedEtcMapEntry expectedEtcMapEntry: expected) {
+                    if(expectedEtcMapEntry.pattern.equals(actualMapEntry.getPattern())) {
+                        expectedFound = expectedEtcMapEntry;
+                        break;
+                    }
+                }
+                if(expectedFound == null) {
+                    TestCase.fail("This pattern (" + actualMapEntry.getPattern() + ") is not expected for: " + title);
+                }
+                expectedFound.assertEtcMap(title, actualMapEntry);
+                expected.remove(expectedFound);
+            }
+            for(ExpectedEtcMapEntry expectedEtcMapEntry: expected) {
+                TestCase.fail("Expected Map Entry (" + expectedEtcMapEntry.pattern + ") not provided for: " + title);
+            }
+        }
+    }
+
+    public static class ExpectedEtcMapEntry {
+        private String pattern;
+        private boolean internal;
+        private String redirect;
+
+        public ExpectedEtcMapEntry(String pattern, String redirect) {
+            this(pattern, false, redirect);
+        }
+
+        public ExpectedEtcMapEntry(String pattern, boolean internal, String redirect) {
+            this.pattern = pattern;
+            this.internal = internal;
+            this.redirect = redirect;
+        }
+
+        public void assertEtcMap(String title, MapEntry mapEntry) {
+            assertEquals("Wrong Pattern for " + title, pattern, mapEntry.getPattern());
+            List<String> givenRedirects = new ArrayList<>(Arrays.asList(mapEntry.getRedirect()));
+            assertEquals("Wrong Number of Redirects for: " + title, 1, givenRedirects.size());
+            assertEquals("Wrong Redirect for: " + title, this.redirect, givenRedirects.get(0));
+            assertEquals("Wrong Redirect Type (ext/int) for: " + title, this.internal, mapEntry.isInternal());
+        }
+    }
+}