blob: deda9028df175f367acbdf8972c2362b09cb3d49 [file] [log] [blame]
/*
* 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.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
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.PathToUriMappingService;
import org.apache.sling.api.resource.mapping.ResourceMapper;
import org.apache.sling.api.uri.SlingUri;
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;
private final PathToUriMappingService pathToUriMappingService;
public ResourceMapperImpl(ResourceResolverImpl resolver, ResourceDecoratorTracker resourceDecorator,
MapEntriesHandler mapEntries, boolean optimizedAliasResolutionEnabled, Object namespaceMangler,
PathToUriMappingService pathToUriMappingService) {
this.resolver = resolver;
this.resourceDecorator = resourceDecorator;
this.mapEntries = mapEntries;
this.optimizedAliasResolutionEnabled = optimizedAliasResolutionEnabled;
this.namespaceMangler = namespaceMangler;
this.pathToUriMappingService = pathToUriMappingService;
}
@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();
// A note on the usage of the 'mappings' variable and the order of the results
//
// 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 take care to add the entries in a very particular order, which preserves backwards
// compatibility with the existing implementation. Previously the order was
//
// resource path → aliases → mapping (with aliases potentially being empty)
//
// To ensure we keep the same behaviour but expose all possible mappings, we now have the following
// flow
//
// resource path → mapping
// resource path → aliases
// aliases → mappings
//
// After all are processed we reverse the order 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 since we will have a small number of
// entries ( <= 4 in case of single aliases) the time spent here should be negligible.
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 '{}'", 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
if ( !mappedPath.isEmpty() )
mappings.add(mappedPath);
// 3. load mappings from the resource path
populateMappingsFromMapEntries(mappings, Collections.singletonList(mappedPath), requestContext);
// 4. load aliases
final Resource nonDecoratedResource = resolver.resolveInternal(parsed.getRawPath(), parsed.getParameters());
if (nonDecoratedResource != null) {
List<String> aliases = loadAliasesIfApplicable(nonDecoratedResource);
// avoid duplicating the originally requested path
aliases.remove(mappedPath);
// ensure that the first declared alias will be returned first
Collections.reverse(aliases);
// 5. load mappings for alias
mappings.addAll(aliases);
populateMappingsFromMapEntries(mappings, aliases, requestContext);
}
// 6. add vanity paths
List<String> vanityPaths = mapEntries.getVanityPathMappings().getOrDefault(mappedPath, Collections.emptyList());
// vanity paths are prepended to make sure they get returned last
mappings.addAll(0, vanityPaths);
// Apply mappings from Resource Mappers
PathToUriMappingService.Result mappingResult = pathToUriMappingService.map(request, mappings.get(mappings.size() - 1));
for (Map.Entry<String, SlingUri> mappingFromChain : mappingResult.getIntermediateMappings().entrySet()) {
mappings.add(mappingFromChain.getValue().toString());
}
// 7. apply context path if needed
mappings.replaceAll(new ApplyContextPath(request));
// 8. set back the fragment query if needed
if ( fragmentQuery != null ) {
mappings.replaceAll(path -> path.concat(fragmentQuery));
}
mappings.forEach( path -> {
logger.debug("map: Returning URL {} as mapping for path {}", path, resourcePath);
});
Collections.reverse(mappings);
return new LinkedHashSet<>(mappings);
}
private List<String> loadAliasesIfApplicable(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
PathGenerator pathBuilder = new PathGenerator();
// make sure to append resolutionPathInfo, if present
pathBuilder.setResolutionPathInfo(resolutionPathInfo);
Resource current = res;
String path = res.getPath();
while (path != null) {
List<String> aliases = Collections.emptyList();
// read alias only if we can read the resources and it's not a jcr:content leaf
if (current != null && !path.endsWith(ResourceResolverImpl.JCR_CONTENT_LEAF)) {
aliases = readAliases(path, current);
}
// build the path from the name segments or aliases
pathBuilder.insertSegment(aliases, ResourceUtil.getName(path));
path = ResourceUtil.getParent(path);
if ("/".equals(path)) {
path = null;
} else if (path != null) {
current = res.getResourceResolver().resolve(path);
}
}
// and then we have the mapped path to work on
List<String> mappedPaths = pathBuilder.generatePaths();
logger.debug("map: Alias mapping resolves to paths {}", mappedPaths);
return mappedPaths;
}
private List<String> readAliases(String path, Resource current) {
if (optimizedAliasResolutionEnabled) {
logger.debug("map: Optimize Alias Resolution is Enabled");
String parentPath = ResourceUtil.getParent(path);
if ( parentPath == null )
return Collections.emptyList();
final Map<String, String> aliases = mapEntries.getAliasMap(parentPath);
if ( aliases == null || !aliases.containsValue(current.getName()) )
return Collections.emptyList();
return aliases.entrySet().stream()
.filter( e -> current.getName().contentEquals(e.getValue()) )
.map( Entry::getKey )
.collect(Collectors.toList());
} else {
logger.debug("map: Optimize Alias Resolution is Disabled");
String[] aliases = ResourceResolverControl.getProperty(current, ResourceResolverImpl.PROP_ALIAS, String[].class);
if ( aliases == null || aliases.length == 0 )
return Collections.emptyList();
if ( aliases.length == 1 )
return Collections.singletonList(aliases[0]);
return Arrays.asList(aliases);
}
}
private void populateMappingsFromMapEntries(List<String> mappings, List<String> mappedPathList,
final RequestContext requestContext) {
boolean mappedPathIsUrl = false;
for ( String mappedPath : mappedPathList ) {
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 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 {})", 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;
}
private String mangleNamespaces(String absPath) {
if ( absPath != null && namespaceMangler != null && namespaceMangler instanceof JcrNamespaceMangler ) {
absPath = ((JcrNamespaceMangler) namespaceMangler).mangleNamespaces(resolver, logger, absPath);
}
return absPath;
}
}
}