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());
+ }
+ }
+}