blob: 67553d8499c2edbd24cc13c1691e9274a6e50e92 [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.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.resourceresolver.impl.ResourceResolverImpl;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The <code>MapEntry</code> class represents a mapping entry in the mapping
* configuration tree at <code>/etc/map</code>.
*
* @see <a href="https://sling.apache.org/documentation/the-sling-engine/mappings-for-resource-resolution.html">Mappings for Resource Resolution</a>
*/
public class MapEntry implements Comparable<MapEntry> {
/** default log */
private final Logger log = LoggerFactory.getLogger(getClass());
private static final Pattern[] URL_WITH_PORT_MATCH = {
Pattern.compile("http/([^/]+)(\\.[^\\d/]+)(/.*)?$"),
Pattern.compile("https/([^/]+)(\\.[^\\d/]+)(/.*)?$") };
private static final String[] URL_WITH_PORT_REPLACEMENT = {
"http/$1$2.80$3", "https/$1$2.443$3" };
private static final Pattern[] PATH_TO_URL_MATCH = {
Pattern.compile("http/([^/]+)\\.80(/.*)?$"),
Pattern.compile("https/([^/]+)\\.443(/.*)?$"),
Pattern.compile("([^/]+)/([^/]+)\\.(\\d+)(/.*)?$"),
Pattern.compile("([^/]+)/([^/]+)(/.*)?$") };
private static final String[] PATH_TO_URL_REPLACEMENT = { "http://$1$2",
"https://$1$2", "$1://$2:$3$4", "$1://$2$3" };
private final Pattern urlPattern;
private final String[] redirect;
private final int status;
private long order;
public static String appendSlash(String path) {
if (!path.endsWith("/")) {
path = path.concat("/");
}
return path;
}
/**
* Returns a string used for matching map entries against the given request
* or URI parts.
*
* @param scheme
* The URI scheme
* @param host
* The host name
* @param port
* The port number. If this is negative, the default value used
* is 80 unless the scheme is "https" in which case the default
* value is 443.
* @param path
* The (absolute) path
* @return The request path string {scheme}://{host}:{port}{path}.
*/
public static String getURI(final String scheme, final String host, final int port,
final String path) {
final StringBuilder sb = new StringBuilder();
sb.append(scheme).append("://").append(host);
if (port > 0 && !(port == 80 && "http".equals(scheme))
&& !(port == 443 && "https".equals(scheme))) {
sb.append(':').append(port);
}
sb.append(path);
return sb.toString();
}
public static String fixUriPath(final String uriPath) {
for (int i = 0; i < URL_WITH_PORT_MATCH.length; i++) {
final Matcher m = URL_WITH_PORT_MATCH[i].matcher(uriPath);
if (m.find()) {
if (!isRegExp(m.replaceAll("$1$2")))
{
return m.replaceAll(URL_WITH_PORT_REPLACEMENT[i]);
}
}
}
return uriPath;
}
/**
* Converts the resolution path of the form http/host.77/the/path into an
* URI of the form http://host:77/the/path where any potential default port
* (80 for http and 443 for https) is actually removed. If the path is just
* a regular path such as /the/path, this method returns <code>null</code>.
*/
public static String toURI(final String uriPath) {
for (int i = 0; i < PATH_TO_URL_MATCH.length; i++) {
final Matcher m = PATH_TO_URL_MATCH[i].matcher(uriPath);
if (m.find()) {
return m.replaceAll(PATH_TO_URL_REPLACEMENT[i]);
}
}
return null;
}
public static MapEntry createResolveEntry(String url, final Resource resource,
final boolean trailingSlash) {
final ValueMap props = resource.adaptTo(ValueMap.class);
if (props != null) {
// ensure the url contains a port number (if possible)
url = fixUriPath(url);
final String redirect = props.get(
MapEntries.PROP_REDIRECT_EXTERNAL, String.class);
if (redirect != null) {
final int status = props
.get(MapEntries.PROP_REDIRECT_EXTERNAL_STATUS,
302);
return new MapEntry(url, status, trailingSlash, 0, redirect);
}
final String[] internalRedirectProps = props.get(ResourceResolverImpl.PROP_REDIRECT_INTERNAL,
String[].class);
final String[] internalRedirect = filterRegExp(internalRedirectProps);
if (internalRedirect != null) {
return new MapEntry(url, -1, trailingSlash, 0, internalRedirect);
}
}
return null;
}
public static List<MapEntry> createMapEntry(String url, final Resource resource,
final boolean trailingSlash) {
final ValueMap props = resource.adaptTo(ValueMap.class);
if (props != null) {
final String redirect = props.get(
MapEntries.PROP_REDIRECT_EXTERNAL, String.class);
if (redirect != null) {
// ignoring external redirects for mapping
LoggerFactory
.getLogger(MapEntry.class)
.info("createMapEntry: Configuration has external redirect to {}; not creating mapping for configuration in {}",
redirect, resource.getPath());
return null;
}
// ignore potential regular expression url
if (isRegExp(url)) {
LoggerFactory
.getLogger(MapEntry.class)
.info("createMapEntry: URL {} contains a regular expression; not creating mapping for configuration in {}",
url, resource.getPath());
return null;
}
// check whether the url is a match hooked to then string end
String endHook = "";
if (url.endsWith("$")) {
endHook = "$";
url = url.substring(0, url.length() - 1);
}
// check whether the url is for ANY_SCHEME_HOST
if (url.startsWith(MapEntries.ANY_SCHEME_HOST)) {
url = url.substring(MapEntries.ANY_SCHEME_HOST.length());
}
final String[] internalRedirect = props
.get(ResourceResolverImpl.PROP_REDIRECT_INTERNAL,
String[].class);
if (internalRedirect != null) {
// check whether the url is considered external or internal
int status = -1;
final String pathUri = toURI(url);
if (pathUri != null) {
url = pathUri;
status = 302;
}
final List<MapEntry> prepEntries = new ArrayList<MapEntry>(
internalRedirect.length);
for (final String redir : internalRedirect) {
if (!redir.contains("$")) {
MapEntry mapEntry = null;
try{
mapEntry = new MapEntry(redir.concat(endHook), status, trailingSlash, 0, url);
}catch (IllegalArgumentException iae){
//ignore this entry
LoggerFactory
.getLogger(MapEntry.class)
.debug("Ignoring mapping due to exception: " + iae.getMessage(), iae);
}
if (mapEntry!=null){
prepEntries.add(mapEntry);
}
}
}
if (prepEntries.size() > 0) {
return prepEntries;
}
}
}
return null;
}
public MapEntry(String url, final int status, final boolean trailingSlash,
final long order, final String... redirect) {
// ensure trailing slashes on redirects if the url
// ends with a trailing slash
if (trailingSlash) {
url = appendSlash(url);
for (int i = 0; i < redirect.length; i++) {
redirect[i] = appendSlash(redirect[i]);
}
}
// ensure pattern is hooked to the start of the string
if (!url.startsWith("^")) {
url = "^".concat(url);
}
try {
this.urlPattern = Pattern.compile(url);
} catch (Exception e){
throw new IllegalArgumentException("Bad url pattern: " + url,e);
}
this.redirect = redirect;
this.status = status;
this.order = order;
}
/**
* Replaces the specified value according to the rules of this entry
*
* @param value the value to replace
* @return a replaced value of <code>null</code> if the value does not match
*/
public @Nullable String[] replace(final @NotNull String value) {
final Matcher m = urlPattern.matcher(value);
if (m.find()) {
final String[] redirects = getRedirect();
final String[] results = new String[redirects.length];
for (int i = 0; i < redirects.length; i++) {
try {
String redirect = redirects[i];
// SLING-7881 - if the value is a selector on the root resource then the
// result will need to remove the trailing slash from the path
if (redirect.length() > 1 && redirect.endsWith("/")) {
if (value.length() > m.end()) {
if ('.' == value.charAt(m.end())) {
//the suffix starts with a dot and the redirect prefix ends with
// a slash so we need to remove the trailing slash from the prefix
// value to make a valid path when they are combined.
//
// for example: http/localhost.8080/.2.json should become /content.2.json
// instead of /content/.2.json
redirect = redirect.substring(0, redirect.length() - 1);
}
}
}
results[i] = m.replaceFirst(redirect);
} catch (final StringIndexOutOfBoundsException siob){
log.debug("Exception while replacing, ignoring entry {} ", redirects[i], siob);
} catch (final IllegalArgumentException iae){
log.debug("Exception while replacing, ignoring entry {} ", redirects[i], iae);
}
}
return results;
}
return null;
}
public String getPattern() {
return urlPattern.toString();
}
public String[] getRedirect() {
return redirect;
}
public boolean isInternal() {
return getStatus() < 0;
}
public int getStatus() {
return status;
}
// ---------- Comparable
public int compareTo(final MapEntry m) {
if (this == m) {
return 0;
}
final String ownPatternString = urlPattern.toString();
final String mPatternString = m.urlPattern.toString();
final int tlen = ownPatternString.length();
final int mlen = mPatternString.length();
if (tlen < mlen) {
return 1;
} else if (tlen > mlen) {
return -1;
}
// lengths are equal, but the entries are not
// so order based on the pattern
int stringComparison = ownPatternString.toString().compareTo(mPatternString);
if (stringComparison == 0 && order != m.order) {
if (m.order > order) {
return 1;
} else {
return -1;
}
}
return stringComparison;
}
// ---------- Object overwrite
@Override
public String toString() {
final StringBuilder buf = new StringBuilder();
buf.append("MapEntry: match:").append(urlPattern);
buf.append(", replacement:");
if (getRedirect().length == 1) {
buf.append(getRedirect()[0]);
} else {
buf.append(Arrays.asList(getRedirect()));
}
if (isInternal()) {
buf.append(", internal");
} else {
buf.append(", status:").append(getStatus());
}
return buf.toString();
}
// ---------- helper
/**
* Returns <code>true</code> if the string contains unescaped regular
* expression special characters '+', '*', '?', '|', '(', '), '[', and ']'
*
* @param string
* @return
*/
private static boolean isRegExp(final String string) {
for (int i = 0; i < string.length(); i++) {
final char c = string.charAt(i);
if (c == '\\') {
i++; // just skip
} else if ("+*?|()[]".indexOf(c) >= 0) {
return true; // assume an unescaped pattern character
}
}
return false;
}
/**
* Returns an array of strings copied from the given strings where any
* regular expressions in the array are removed. If the input is
* <code>null</code> or no strings are provided <code>null</code> is
* returned. <code>null</code> is also returned if the strings only contain
* regular expressions.
*/
private static String[] filterRegExp(final String... strings) {
if (strings == null || strings.length == 0) {
return null;
}
ArrayList<String> list = new ArrayList<String>(strings.length);
for (String string : strings) {
if (!isRegExp(string)) {
list.add(string);
}
}
return list.isEmpty() ? null : (String[]) list.toArray(new String[list.size()]);
}
void setOrder(long order) {
this.order = order;
}
}