This is an automated email from the ASF dual-hosted git repository. rombert pushed a commit to branch feature/SLING-7792 in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-resourceresolver.git
commit 33d2da96e1d7fee6f67ea46c1c2edec97a7fbd0c Author: Robert Munteanu <[email protected]> AuthorDate: Tue Aug 14 16:05:32 2018 +0200 SLING-7792 - Resource Resolver should return more than one resolved path if available Add a new ResourceMapperImpl class that contains the logic for fine-grained access to resource mappings. Added a new set of tests that validate that the usage of the various mapping sources (aliases, /etc/map) is correct. This change also opens up some methods from the ResourceResolverImpl, but it looks reasonable as they are not exposed outside the bundle. --- .../impl/ResourceResolverImpl.java | 220 +----------- .../impl/mapping/ResourceMapperImpl.java | 368 +++++++++++++++++++++ .../impl/mapping/InMemoryResource.java | 96 ++++++ .../impl/mapping/InMemoryResourceProvider.java | 79 +++++ .../impl/mapping/ResourceMapperImplTest.java | 267 +++++++++++++++ 5 files changed, 823 insertions(+), 207 deletions(-) diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverImpl.java b/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverImpl.java index 4acf759..21ca9c9 100644 --- a/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverImpl.java +++ b/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverImpl.java @@ -25,7 +25,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; @@ -48,6 +47,7 @@ import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.api.resource.ResourceResolverFactory; import org.apache.sling.api.resource.ResourceUtil; import org.apache.sling.api.resource.ResourceWrapper; +import org.apache.sling.api.resource.mapping.ResourceMapper; import org.apache.sling.resourceresolver.impl.helper.RedirectResource; import org.apache.sling.resourceresolver.impl.helper.ResourceIteratorDecorator; import org.apache.sling.resourceresolver.impl.helper.ResourcePathIterator; @@ -57,13 +57,14 @@ import org.apache.sling.resourceresolver.impl.helper.StarResource; import org.apache.sling.resourceresolver.impl.helper.URI; import org.apache.sling.resourceresolver.impl.helper.URIException; import org.apache.sling.resourceresolver.impl.mapping.MapEntry; +import org.apache.sling.resourceresolver.impl.mapping.ResourceMapperImpl; import org.apache.sling.resourceresolver.impl.params.ParsedParameters; import org.apache.sling.resourceresolver.impl.providers.ResourceProviderStorageProvider; import org.apache.sling.spi.resource.provider.ResourceProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -@Adaptable(adaptableClass = ResourceResolver.class, adapters = { @Adapter(Session.class) }) +@Adaptable(adaptableClass = ResourceResolver.class, adapters = { @Adapter(Session.class), @Adapter(ResourceMapper.class) }) public class ResourceResolverImpl extends SlingAdaptable implements ResourceResolver { /** Default logger */ @@ -79,7 +80,7 @@ public class ResourceResolverImpl extends SlingAdaptable implements ResourceReso // such as nt:file. The slash is included to prevent false // positives for the String.endsWith check for names like // "xyzjcr:content" - private static final String JCR_CONTENT_LEAF = "/jcr:content"; + public static final String JCR_CONTENT_LEAF = "/jcr:content"; /** The factory which created this resource resolver. */ private final CommonResourceResolverFactoryImpl factory; @@ -181,7 +182,7 @@ public class ResourceResolverImpl extends SlingAdaptable implements ResourceReso * @throws IllegalStateException * If the resolver is already closed or the factory is no longer live. */ - private void checkClosed() { + public void checkClosed() { if (this.control.isClosed()) { if (closedResolverException != null) { logger.error("The ResourceResolver has already been closed.", closedResolverException); @@ -415,200 +416,7 @@ public class ResourceResolverImpl extends SlingAdaptable implements ResourceReso */ @Override public String map(final HttpServletRequest request, final String resourcePath) { - checkClosed(); - - // find a fragment or query - int fragmentQueryMark = resourcePath.indexOf('#'); - if (fragmentQueryMark < 0) { - fragmentQueryMark = resourcePath.indexOf('?'); - } - - // cut fragment or query off the resource path - String mappedPath; - final String fragmentQuery; - if (fragmentQueryMark >= 0) { - fragmentQuery = resourcePath.substring(fragmentQueryMark); - mappedPath = resourcePath.substring(0, fragmentQueryMark); - logger.debug("map: Splitting resource path '{}' into '{}' and '{}'", new Object[] { resourcePath, mappedPath, - fragmentQuery }); - } else { - fragmentQuery = null; - mappedPath = resourcePath; - } - - // cut off scheme and host, if the same as requested - final String schemehostport; - final String schemePrefix; - if (request != null) { - schemehostport = MapEntry.getURI(request.getScheme(), request.getServerName(), request.getServerPort(), "/"); - schemePrefix = request.getScheme().concat("://"); - logger.debug("map: Mapping path {} for {} (at least with scheme prefix {})", new Object[] { resourcePath, - schemehostport, schemePrefix }); - - } else { - - schemehostport = null; - schemePrefix = null; - logger.debug("map: Mapping path {} for default", resourcePath); - - } - - ParsedParameters parsed = new ParsedParameters(mappedPath); - final Resource nonDecoratedResource = resolveInternal(parsed.getRawPath(), parsed.getParameters()); - - if (nonDecoratedResource != null) { - - //Invoke the decorator for the resolved resource - Resource res=this.factory.getResourceDecoratorTracker().decorate(nonDecoratedResource); - - // keep, what we might have cut off in internal resolution - final String resolutionPathInfo = res.getResourceMetadata().getResolutionPathInfo(); - - logger.debug("map: Path maps to resource {} with path info {}", res, resolutionPathInfo); - - // find aliases for segments. we can't walk the parent chain - // since the request session might not have permissions to - // read all parents SLING-2093 - final LinkedList<String> names = new LinkedList<>(); - - Resource current = res; - String path = res.getPath(); - while (path != null) { - String alias = null; - if (current != null && !path.endsWith(JCR_CONTENT_LEAF)) { - if (factory.isOptimizeAliasResolutionEnabled()) { - logger.debug("map: Optimize Alias Resolution is Enabled"); - String parentPath = ResourceUtil.getParent(path); - if (parentPath != null) { - final Map<String, String> aliases = factory.getMapEntries().getAliasMap(parentPath); - if (aliases!= null && aliases.containsValue(current.getName())) { - for (String key:aliases.keySet()) { - if (current.getName().equals(aliases.get(key))) { - alias = key; - break; - } - } - } - } - } else { - logger.debug("map: Optimize Alias Resolution is Disabled"); - alias = ResourceResolverControl.getProperty(current, PROP_ALIAS); - } - } - if (alias == null || alias.length() == 0) { - alias = ResourceUtil.getName(path); - } - names.add(alias); - path = ResourceUtil.getParent(path); - if ("/".equals(path)) { - path = null; - } else if (path != null) { - current = res.getResourceResolver().resolve(path); - } - } - - // build path from segment names - final StringBuilder buf = new StringBuilder(); - - // construct the path from the segments (or root if none) - if (names.isEmpty()) { - buf.append('/'); - } else { - while (!names.isEmpty()) { - buf.append('/'); - buf.append(names.removeLast()); - } - } - - // reappend the resolutionPathInfo - if (resolutionPathInfo != null) { - buf.append(resolutionPathInfo); - } - - // and then we have the mapped path to work on - mappedPath = buf.toString(); - - logger.debug("map: Alias mapping resolves to path {}", mappedPath); - - } - - boolean mappedPathIsUrl = false; - for (final MapEntry mapEntry : this.factory.getMapEntries().getMapMaps()) { - final String[] mappedPaths = mapEntry.replace(mappedPath); - if (mappedPaths != null) { - - logger.debug("map: Match for Entry {}", mapEntry); - - mappedPathIsUrl = !mapEntry.isInternal(); - - if (mappedPathIsUrl && schemehostport != null) { - - mappedPath = null; - - for (final String candidate : mappedPaths) { - if (candidate.startsWith(schemehostport)) { - mappedPath = candidate.substring(schemehostport.length() - 1); - mappedPathIsUrl = false; - logger.debug("map: Found host specific mapping {} resolving to {}", candidate, mappedPath); - break; - } else if (candidate.startsWith(schemePrefix) && mappedPath == null) { - mappedPath = candidate; - } - } - - if (mappedPath == null) { - mappedPath = mappedPaths[0]; - } - - } else { - - // we can only go with assumptions selecting the first entry - mappedPath = mappedPaths[0]; - - } - - logger.debug("map: MapEntry {} matches, mapped path is {}", mapEntry, mappedPath); - - break; - } - } - - // this should not be the case, since mappedPath is primed - if (mappedPath == null) { - mappedPath = resourcePath; - } - - // [scheme:][//authority][path][?query][#fragment] - try { - // use commons-httpclient's URI instead of java.net.URI, as it can - // actually accept *unescaped* URIs, such as the "mappedPath" and - // return them in proper escaped form, including the path, via - // toString() - final URI uri = new URI(mappedPath, false); - - // 1. mangle the namespaces in the path - String path = mangleNamespaces(uri.getPath()); - - // 2. prepend servlet context path if we have a request - if (request != null && request.getContextPath() != null && request.getContextPath().length() > 0) { - path = request.getContextPath().concat(path); - } - // update the path part of the URI - uri.setPath(path); - - mappedPath = uri.toString(); - } catch (final URIException e) { - logger.warn("map: Unable to mangle namespaces for " + mappedPath + " returning unmangled", e); - } - - logger.debug("map: Returning URL {} as mapping for path {}", mappedPath, resourcePath); - - // reappend fragment and/or query - if (fragmentQuery != null) { - mappedPath = mappedPath.concat(fragmentQuery); - } - - return mappedPath; + return adaptTo(ResourceMapper.class).getMapping(resourcePath, request); } // ---------- search path for relative resoures @@ -805,6 +613,7 @@ public class ResourceResolverImpl extends SlingAdaptable implements ResourceReso /** * @see org.apache.sling.api.adapter.SlingAdaptable#adaptTo(java.lang.Class) */ + @SuppressWarnings("unchecked") @Override public <AdapterType> AdapterType adaptTo(final Class<AdapterType> type) { checkClosed(); @@ -812,6 +621,11 @@ public class ResourceResolverImpl extends SlingAdaptable implements ResourceReso if (type.getName().equals("javax.jcr.Session")) { return getSession(type); } + + if ( type == ResourceMapper.class ) + return (AdapterType) new ResourceMapperImpl(this, factory.getResourceDecoratorTracker(), factory.getMapEntries(), + factory.isOptimizeAliasResolutionEnabled(), factory.getNamespaceMangler()); + final AdapterType result = this.control.adaptTo(this.context, type); if ( result != null ) { return result; @@ -873,7 +687,7 @@ public class ResourceResolverImpl extends SlingAdaptable implements ResourceReso * the part of the <code>absPath</code> which has been cut off by * the {@link ResourcePathIterator} to resolve the resource. */ - private Resource resolveInternal(final String absPath, final Map<String, String> parameters) { + public Resource resolveInternal(final String absPath, final Map<String, String> parameters) { Resource resource = null; String curPath = absPath; try { @@ -1102,14 +916,6 @@ public class ResourceResolverImpl extends SlingAdaptable implements ResourceReso return path; } - private String mangleNamespaces(String absPath) { - if ( absPath != null && factory.getNamespaceMangler() != null ) { - absPath = ((JcrNamespaceMangler)factory.getNamespaceMangler()).mangleNamespaces(this, logger, absPath); - } - - return absPath; - } - private String unmangleNamespaces(String absPath) { if (absPath != null && factory.getNamespaceMangler() != null ) { absPath = ((JcrNamespaceMangler)factory.getNamespaceMangler()).unmangleNamespaces(this, logger, absPath); diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/mapping/ResourceMapperImpl.java b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/ResourceMapperImpl.java new file mode 100644 index 0000000..5610054 --- /dev/null +++ b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/ResourceMapperImpl.java @@ -0,0 +1,368 @@ +/* + * 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 java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.function.UnaryOperator; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceUtil; +import org.apache.sling.api.resource.mapping.ResourceMapper; +import org.apache.sling.resourceresolver.impl.JcrNamespaceMangler; +import org.apache.sling.resourceresolver.impl.ResourceResolverImpl; +import org.apache.sling.resourceresolver.impl.helper.ResourceDecoratorTracker; +import org.apache.sling.resourceresolver.impl.helper.ResourceResolverControl; +import org.apache.sling.resourceresolver.impl.helper.URI; +import org.apache.sling.resourceresolver.impl.helper.URIException; +import org.apache.sling.resourceresolver.impl.params.ParsedParameters; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ResourceMapperImpl implements ResourceMapper { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final ResourceResolverImpl resolver; + private final ResourceDecoratorTracker resourceDecorator; + private final MapEntriesHandler mapEntries; + private final boolean optimizedAliasResolutionEnabled; + private final Object namespaceMangler; + + + public ResourceMapperImpl(ResourceResolverImpl resolver, ResourceDecoratorTracker resourceDecorator, + MapEntriesHandler mapEntries, boolean optimizedAliasResolutionEnabled, Object namespaceMangler) { + this.resolver = resolver; + this.resourceDecorator = resourceDecorator; + this.mapEntries = mapEntries; + this.optimizedAliasResolutionEnabled = optimizedAliasResolutionEnabled; + this.namespaceMangler = namespaceMangler; + } + + @Override + public String getMapping(String resourcePath) { + return getMapping(resourcePath, null); + } + + @Override + public String getMapping(String resourcePath, HttpServletRequest request) { + + Collection<String> mappings = getAllMappings(resourcePath, request); + if ( mappings.isEmpty() ) + return null; + + return mappings.iterator().next(); + } + + @Override + public Collection<String> getAllMappings(String resourcePath) { + return getAllMappings(resourcePath, null); + } + + @Override + public Collection<String> getAllMappings(String resourcePath, HttpServletRequest request) { + + resolver.checkClosed(); + + List<String> mappings = new ArrayList<>(); + + // 1. parse parameters + + // find a fragment or query + int fragmentQueryMark = resourcePath.indexOf('#'); + if (fragmentQueryMark < 0) { + fragmentQueryMark = resourcePath.indexOf('?'); + } + + // cut fragment or query off the resource path + String mappedPath; + final String fragmentQuery; + if (fragmentQueryMark >= 0) { + fragmentQuery = resourcePath.substring(fragmentQueryMark); + mappedPath = resourcePath.substring(0, fragmentQueryMark); + logger.debug("map: Splitting resource path '{}' into '{}' and '{}'", new Object[] { resourcePath, mappedPath, + fragmentQuery }); + } else { + fragmentQuery = null; + mappedPath = resourcePath; + } + + final RequestContext requestContext = new RequestContext(request, resourcePath); + ParsedParameters parsed = new ParsedParameters(mappedPath); + + // 2. add the requested path itself + mappings.add(mappedPath); + + + // 3. load aliases + final Resource nonDecoratedResource = resolver.resolveInternal(parsed.getRawPath(), parsed.getParameters()); + if (nonDecoratedResource != null) { + loadAliasIfApplicable(mappings, nonDecoratedResource); + } + + // 4. load /etc/map entries + // populate entries from all entries, including original path and any found aliases + for ( String mapped : new ArrayList<>(mappings) ) + populateMappingsFromMapEntries(mappings, mapped, requestContext); + + + // 5. apply context path if needed + mappings.replaceAll(new ApplyContextPath(request)); + + // 6. set back the fragment query if needed + if ( fragmentQuery != null ) { + mappings.replaceAll(new UnaryOperator<String>() { + @Override + public String apply(String mappedPath) { + return mappedPath.concat(fragmentQuery); + } + }); + } + + mappings.forEach( path -> { + logger.debug("map: Returning URL {} as mapping for path {}", path, resourcePath); + }); + + + // The API contract of the ResourceMapper does not specify the order in which the elements are returned + // As an implementation detail however the getMapping method picks the first element of the return value + // as the 'winner'. + // + // Therefore we reverse the sorting to preserve the logic of the old ResourceResolver.map() method (last + // found wins) and also make sure that no duplicates are added + // + // There is some room for improvement here by using a data structure that does not need reversing ( ArrayList + // .add moves the elements every time ) or reversal of duplicates but the expectation is that we have a small + // number of mappings ( <= 10 ) so the time spent here should be negligible. + + Collections.reverse(mappings); + + return new LinkedHashSet<>(mappings); + } + + private void loadAliasIfApplicable(List<String> mappings, final Resource nonDecoratedResource) { + //Invoke the decorator for the resolved resource + Resource res = resourceDecorator.decorate(nonDecoratedResource); + + // keep, what we might have cut off in internal resolution + final String resolutionPathInfo = res.getResourceMetadata().getResolutionPathInfo(); + + logger.debug("map: Path maps to resource {} with path info {}", res, resolutionPathInfo); + + // find aliases for segments. we can't walk the parent chain + // since the request session might not have permissions to + // read all parents SLING-2093 + final LinkedList<String> names = new LinkedList<>(); + + Resource current = res; + String path = res.getPath(); + while (path != null) { + String alias = null; + if (current != null && !path.endsWith(ResourceResolverImpl.JCR_CONTENT_LEAF)) { + if (optimizedAliasResolutionEnabled) { + logger.debug("map: Optimize Alias Resolution is Enabled"); + String parentPath = ResourceUtil.getParent(path); + if (parentPath != null) { + final Map<String, String> aliases = mapEntries.getAliasMap(parentPath); + if (aliases!= null && aliases.containsValue(current.getName())) { + for (String key:aliases.keySet()) { + if (current.getName().equals(aliases.get(key))) { + alias = key; + break; + } + } + } + } + } else { + logger.debug("map: Optimize Alias Resolution is Disabled"); + alias = ResourceResolverControl.getProperty(current, ResourceResolverImpl.PROP_ALIAS); + } + } + if (alias == null || alias.length() == 0) { + alias = ResourceUtil.getName(path); + } + names.add(alias); + path = ResourceUtil.getParent(path); + if ("/".equals(path)) { + path = null; + } else if (path != null) { + current = res.getResourceResolver().resolve(path); + } + } + + // build path from segment names + final StringBuilder buf = new StringBuilder(); + + // construct the path from the segments (or root if none) + if (names.isEmpty()) { + buf.append('/'); + } else { + while (!names.isEmpty()) { + buf.append('/'); + buf.append(names.removeLast()); + } + } + + // reappend the resolutionPathInfo + if (resolutionPathInfo != null) { + buf.append(resolutionPathInfo); + } + + // and then we have the mapped path to work on + String mappedPath = buf.toString(); + + logger.debug("map: Alias mapping resolves to path {}", mappedPath); + + mappings.add(mappedPath); + } + + private void populateMappingsFromMapEntries(List<String> mappings, String mappedPath, + final RequestContext requestContext) { + boolean mappedPathIsUrl = false; + for (final MapEntry mapEntry : mapEntries.getMapMaps()) { + final String[] mappedPaths = mapEntry.replace(mappedPath); + if (mappedPaths != null) { + + logger.debug("map: Match for Entry {}", mapEntry); + + mappedPathIsUrl = !mapEntry.isInternal(); + + if (mappedPathIsUrl && requestContext.hasUri() ) { + + mappedPath = null; + + for (final String candidate : mappedPaths) { + if (candidate.startsWith(requestContext.getUri())) { + mappedPath = candidate.substring(requestContext.getUri().length() - 1); + mappedPathIsUrl = false; + logger.debug("map: Found host specific mapping {} resolving to {}", candidate, mappedPath); + break; + } else if (candidate.startsWith(requestContext.getSchemeWithPrefix()) && mappedPath == null) { + mappedPath = candidate; + } + } + + if (mappedPath == null) { + mappedPath = mappedPaths[0]; + } + + } else { + + // we can only go with assumptions selecting the first entry + mappedPath = mappedPaths[0]; + + } + + logger.debug("map: MapEntry {} matches, mapped path is {}", mapEntry, mappedPath); + + mappings.add(mappedPath); + + break; + } + } + } + + private String mangleNamespaces(String absPath) { + if ( absPath != null && namespaceMangler != null && namespaceMangler instanceof JcrNamespaceMangler ) { + absPath = ((JcrNamespaceMangler) namespaceMangler).mangleNamespaces(resolver, logger, absPath); + } + + return absPath; + } + + private class RequestContext { + + private final String uri; + private final String schemeWithPrefix; + + private RequestContext(HttpServletRequest request, String resourcePath) { + if ( request != null ) { + this.uri = MapEntry.getURI(request.getScheme(), request.getServerName(), request.getServerPort(), "/"); + this.schemeWithPrefix = request.getScheme().concat("://"); + logger.debug("map: Mapping path {} for {} (at least with scheme prefix {})", new Object[] { resourcePath, + uri, schemeWithPrefix }); + } else { + this.uri = null; + this.schemeWithPrefix = null; + logger.debug("map: Mapping path {} for default", resourcePath); + } + + } + + public String getUri() { + return uri; + } + + public String getSchemeWithPrefix() { + return schemeWithPrefix; + } + + public boolean hasUri() { + return uri != null && schemeWithPrefix != null; + } + } + + private class ApplyContextPath implements UnaryOperator<String> { + + private final HttpServletRequest req; + + private ApplyContextPath(HttpServletRequest req) { + this.req = req; + } + + @Override + public String apply(String path) { + + String mappedPath = path; + + // [scheme:][//authority][path][?query][#fragment] + try { + // use commons-httpclient's URI instead of java.net.URI, as it can + // actually accept *unescaped* URIs, such as the "mappedPath" and + // return them in proper escaped form, including the path, via + // toString() + final URI uri = new URI(path, false); + + // 1. mangle the namespaces in the path + path = mangleNamespaces(uri.getPath()); + + // 2. prepend servlet context path if we have a request + if (req != null && req.getContextPath() != null && req.getContextPath().length() > 0) { + path = req.getContextPath().concat(path); + } + // update the path part of the URI + uri.setPath(path); + + mappedPath = uri.toString(); + } catch (final URIException e) { + logger.warn("map: Unable to mangle namespaces for " + mappedPath + " returning unmangled", e); + } + + return mappedPath; + } + + } +} diff --git a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/InMemoryResource.java b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/InMemoryResource.java new file mode 100644 index 0000000..9792ca9 --- /dev/null +++ b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/InMemoryResource.java @@ -0,0 +1,96 @@ +/* + * 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 java.util.HashMap; +import java.util.Map; + +import org.apache.sling.api.resource.AbstractResource; +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; + +public class InMemoryResource extends AbstractResource { + + private final String path; + private final ResourceMetadata metadata; + private final ResourceResolver resolver; + private final Map<String, Object> properties = new HashMap<>(); + + public InMemoryResource(String path, ResourceResolver resolver, Map<String, Object> properties) { + + if ( path == null ) + throw new IllegalArgumentException("path is null"); + + if ( resolver == null ) + throw new IllegalArgumentException("resovler is null"); + + this.path = path; + this.metadata = new ResourceMetadata(); + this.metadata.setResolutionPath(path);; + this.resolver = resolver; + this.properties.putAll(properties); + } + + @Override + public String getPath() { + return path; + } + + @Override + public String getResourceType() { + return getValueMap().get("sling:resourceType", String.class); + } + + @Override + public String getResourceSuperType() { + return getValueMap().get("sling:resourceSuperType", String.class); + } + + @Override + public ResourceMetadata getResourceMetadata() { + return metadata; + } + + @Override + public ResourceResolver getResourceResolver() { + return resolver; + } + + @Override + @SuppressWarnings("unchecked") + public <AdapterType> AdapterType adaptTo(Class<AdapterType> type) { + if(type == ValueMap.class || type == Map.class) { + return (AdapterType) new ValueMapDecorator(properties); + } + return super.adaptTo(type); + } + + public InMemoryResource set(String prop, Object val) { + properties.put(prop, val); + + return this; + } + + @Override + public String toString() { + return getClass().getSimpleName() + " : [ path = " + path + " ]"; + } +} \ No newline at end of file diff --git a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/InMemoryResourceProvider.java b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/InMemoryResourceProvider.java new file mode 100644 index 0000000..956a25d --- /dev/null +++ b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/InMemoryResourceProvider.java @@ -0,0 +1,79 @@ +/* + * 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 java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceUtil; +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.osgi.service.component.annotations.Component; + +@Component(service = ResourceProvider.class) +public class InMemoryResourceProvider extends ResourceProvider<Void> { + + private final Map<String, Map<String, Object>> resources = new HashMap<>(); + + @Override + public Resource getResource(ResolveContext<Void> ctx, String path, ResourceContext resourceContext, + Resource parent) { + + Map<String, Object> vals = resources.get(path); + if ( vals == null ) + return null; + + return new InMemoryResource(path, ctx.getResourceResolver(), vals); + + } + + @Override + public Iterator<Resource> listChildren(ResolveContext<Void> ctx, Resource parent) { + + return resources.entrySet().stream() + .filter( e -> parent.getPath().equals(ResourceUtil.getParent(e.getKey())) ) + .map( e -> (Resource) new InMemoryResource(e.getKey(), ctx.getResourceResolver(), e.getValue()) ) + .iterator(); + } + + public void putResource(String path) { + putResource(path, Collections.emptyMap()); + } + + public void putResource(String path, String key, Object value) { + putResource(path, Collections.singletonMap(key, value)); + } + + public void putResource(String path, String key, Object value, String key2, Object value2) { + Map<String, Object> props = new HashMap<>(); + props.put(key, value); + props.put(key2, value2); + putResource(path, props); + } + + public void putResource(String path, Map<String, Object> props) { + resources.put(path, props); + } + +} \ No newline at end of file diff --git a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/ResourceMapperImplTest.java b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/ResourceMapperImplTest.java new file mode 100644 index 0000000..6caefa9 --- /dev/null +++ b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/ResourceMapperImplTest.java @@ -0,0 +1,267 @@ +/* + * 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 static org.apache.sling.resourceresolver.impl.ResourceResolverImpl.PROP_ALIAS; +import static org.apache.sling.spi.resource.provider.ResourceProvider.PROPERTY_NAME; +import static org.apache.sling.spi.resource.provider.ResourceProvider.PROPERTY_ROOT; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.sling.api.resource.LoginException; +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.ResourceUtil; +import org.apache.sling.api.resource.mapping.ResourceMapper; +import org.apache.sling.resourceresolver.impl.ResourceAccessSecurityTracker; +import org.apache.sling.resourceresolver.impl.ResourceResolverFactoryActivator; +import org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl; +import org.apache.sling.spi.resource.provider.ResourceProvider; +import org.apache.sling.testing.mock.osgi.junit.OsgiContext; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +/** + * Validates that the {@link ResourceMapperImpl} correctly queries all sources of mappings + * + * <p>This test ensures that in case more than one mappings is possible, for instance: + * + * <ol> + * <li>path for an existing resource</li> + * <li>alias</li> + * <li>/etc/map entries</li> + * </ol> + * + * all are correctly considered and included in the relevant method calls. + * </p> + * + * <p>This test should not exhaustively test all mapping scenarios, other tests in this + * module and the Sling ITs cover that.</p> + * + */ +public class ResourceMapperImplTest { + + @Rule + public final OsgiContext ctx = new OsgiContext(); + + private HttpServletRequest req; + private ResourceResolver resolver; + + @Before + public void prepare() throws LoginException { + + ctx.registerInjectActivateService(new ServiceUserMapperImpl()); + ctx.registerInjectActivateService(new ResourceAccessSecurityTracker()); + + InMemoryResourceProvider resourceProvider = new InMemoryResourceProvider(); + resourceProvider.putResource("/"); // root + resourceProvider.putResource("/here"); // regular page + resourceProvider.putResource("/there", PROP_ALIAS, "alias-value"); // with alias + resourceProvider.putResource("/somewhere", PROP_ALIAS, "alias-value-2"); // with alias and also /etc/map + + // build /etc/map structure + resourceProvider.putResource("/etc"); + resourceProvider.putResource("/etc/map"); + resourceProvider.putResource("/etc/map/http"); + resourceProvider.putResource("/etc/map/http/localhost_any", + "sling:internalRedirect", "/somewhere", + "sling:match", "localhost.8080/everywhere"); + + // we fake the fact that we are the JCR resource provider since it's the required one + ctx.registerService(ResourceProvider.class, resourceProvider, PROPERTY_ROOT, "/", PROPERTY_NAME, "JCR"); + // disable optimised alias resolution as it relies on JCR queries + ctx.registerInjectActivateService(new ResourceResolverFactoryActivator(), + "resource.resolver.optimize.alias.resolution", false); + + ResourceResolverFactory factory = ctx.getService(ResourceResolverFactory.class); + + assertNotNull(factory); + + resolver = factory.getResourceResolver(null); + + req = mock(HttpServletRequest.class); + when(req.getScheme()).thenReturn("http"); + when(req.getServerName()).thenReturn("localhost"); + when(req.getServerPort()).thenReturn(8080); + when(req.getContextPath()).thenReturn("/app"); + } + + @After + public void cleanup() { + if ( resolver != null ) + resolver.close(); + } + + /** + * Validates that mappings for a non-existing resource only contain that resource's path + * + * @throws LoginException + */ + @Test + public void mapNonExistingPath() throws LoginException { + + ExpectedMappings.nonExistingResource("/not-here") + .singleMapping("/not-here") + .singleMappingWithRequest("/app/not-here") + .allMappings("/not-here") + .allMappingsWithRequest("/app/not-here") + .verify(resolver, req); + } + + /** + * Validates that mappings for an existing resource only contain that resource's path + * + * @throws LoginException + */ + @Test + public void mapExistingPath() throws LoginException { + + ExpectedMappings.existingResource("/here") + .singleMapping("/here") + .singleMappingWithRequest("/app/here") + .allMappings("/here") + .allMappingsWithRequest("/app/here") + .verify(resolver, req); + } + + /** + * Validates that mappings for a existing resource with an alias contain the alias and the resource's path + * + * @throws LoginException + */ + @Test + public void mapResourceWithAlias() { + + ExpectedMappings.existingResource("/there") + .singleMapping("/alias-value") + .singleMappingWithRequest("/app/alias-value") + .allMappings("/alias-value", "/there") + .allMappingsWithRequest("/app/alias-value", "/app/there") + .verify(resolver, req); + } + + /** + * Validates that mappings for a existing resource with an alias and /etc/map entry + * contain the /etc/map entry, the alias and the resource's path + * + * @throws LoginException + */ + @Test + public void mapResourceWithAliasAndEtcMap() { + + ExpectedMappings.existingResource("/somewhere") + .singleMapping("http://localhost:8080/everywhere") + .singleMappingWithRequest("/app/everywhere") + .allMappings("http://localhost:8080/everywhere", "/alias-value-2", "/somewhere") + .allMappingsWithRequest("/app/everywhere","/app/alias-value-2", "/app/somewhere") + .verify(resolver, req); + } + + static class ExpectedMappings { + + public static ExpectedMappings existingResource(String path) { + return new ExpectedMappings(path, true); + } + + public static ExpectedMappings nonExistingResource(String path) { + return new ExpectedMappings(path, false); + } + + private final String path; + private final boolean exists; + + private String singleMapping; + private String singleMappingWithRequest; + private Set<String> allMappings; + private Set<String> allMappingsWithRequest; + + private ExpectedMappings(String path, boolean exists) { + this.path = path; + this.exists = exists; + } + + public ExpectedMappings singleMapping(String singleMapping) { + this.singleMapping = singleMapping; + + return this; + } + + public ExpectedMappings singleMappingWithRequest(String singleMappingWithRequest) { + this.singleMappingWithRequest = singleMappingWithRequest; + + return this; + } + + public ExpectedMappings allMappings(String... allMappings) { + this.allMappings = new HashSet<>(Arrays.asList(allMappings)); + + return this; + } + + public ExpectedMappings allMappingsWithRequest(String... allMappingsWithRequest) { + this.allMappingsWithRequest = new HashSet<>(Arrays.asList(allMappingsWithRequest)); + + return this; + } + + public void verify(ResourceResolver resolver, HttpServletRequest request) { + checkConfigured(); + + Resource res = resolver.getResource(path); + if ( exists ) { + assertThat("Resource was null but should exist", res, notNullValue()); + assertThat("Resource is non-existing but should exist", ResourceUtil.isNonExistingResource(res), is(false)); + } else { + assertThat("Resource is neither null nor non-existing", res == null || ResourceUtil.isNonExistingResource(res), is(true)); + } + + // downcast to ensure we're testing the right class + ResourceMapperImpl mapper = (ResourceMapperImpl) resolver.adaptTo(ResourceMapper.class); + + assertThat("Single mapping without request", mapper.getMapping(path), is(singleMapping)); + assertThat("Single mapping with request", mapper.getMapping(path, request), is(singleMappingWithRequest)); + assertThat("All mappings without request", mapper.getAllMappings(path), is(allMappings)); + assertThat("All mappings with request", mapper.getAllMappings(path, request), is(allMappingsWithRequest)); + } + + private void checkConfigured() { + if ( singleMapping == null ) + throw new IllegalStateException("singleMapping is null"); + if ( singleMappingWithRequest == null ) + throw new IllegalStateException("singleMappingWithRequest is null"); + if ( allMappings == null ) + throw new IllegalStateException("allMappings is null"); + if ( allMappingsWithRequest == null ) + throw new IllegalStateException("allMappingsWithRequest is null"); + } + } +}
