All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.composum.sling.core.util.SlingUrl Maven / Gradle / Ivy

There is a newer version: 4.3.4
Show newest version
package com.composum.sling.core.util;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.servlet.http.HttpServletRequest;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.composum.sling.core.util.LinkUtil.adjustMappedUrl;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

/**
 * A Sling URL parser / builder class supporting the
 * Sling URL decomposition
 * (and composition / modification by using the builder methods) and provides builder methods to change e.g. selectors and extension,
 * but can also represent other URL types.
 * It is meant to represent every user input without failing - if it's not a known URL scheme and thus cannot be parsed
 * it'll just return the input unchanged, and the modification methods fail silently.
 * 

Major usecases

*

Building URLs for resources

*

* With the constructors with a {@link Resource} parameter, e.g. {@link #SlingUrl(SlingHttpServletRequest, Resource)}, * it's possible to generate an URL and give it some {@link #selectors(String)}, {@link #extension(String)} or * and {@link #suffix(Resource)} and {@link #parameter(String, String...)}. If only the path of the resource is known, * that'd start with {@link #SlingUrl(SlingHttpServletRequest)} and initalized with {@link #fromPath(String)}. *

*

Modifying userspecified URLs

* The class is meant to represent all types of URL (including invalid ones), and parse them in a do-what-I-mean fashion, * so that they can be modified according to Sling rules, as far as possible. There are direct constructors from a Resource, * and various ways to initialize it mit a method starting with from (e.g. {@link #fromPathOrUrl(String)}) * after creating a basic builder with {@link #SlingUrl(SlingHttpServletRequest)} or {@link #SlingUrl(SlingHttpServletRequest, LinkMapper)}. *

* Caution: this does not consider the resource tree to parse URLs from String form, so there will be cases * where this differs from {@link ResourceResolver#resolve(HttpServletRequest, String)} : e.g. we consider * in /foo/a.b/bar/c.d the /bar/c.d as suffix, while this might be different in reality! *

* * @see "https://sling.apache.org/documentation/the-sling-engine/url-decomposition.html" * @see "https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/URI.html" */ @SuppressWarnings({"unused", "ParameterHidesMemberVariable", "UnusedReturnValue"}) public class SlingUrl implements Cloneable { private static final Logger LOG = LoggerFactory.getLogger(SlingUrl.class); /** * Characterizes the type of the URL. */ public enum UrlType { /** * HTTP(S) Sling URL (or other URL, since these cannot necessarily be distinguished). * It can have a scheme ({@link #isExternal()}) or be an absolute path without a scheme - in this case * it'll be turned into a HTTP(S) URL on {@link #getUrl()} by the {@link LinkMapper}. */ HTTP, /** * A file or ftp (file transfer protocol) URL. */ FILE, /** * A relative URL - has no scheme but represents a (relative) path which possibly has selectors, extension, suffix etc. */ RELATIVE, /** * Special URL format that doesn't have a path, like mailto: or tel: . * Since it cannot be parsed, the get-Methods mostly do not provide a value here, except {@link #getUrl()}. */ SPECIAL, /** * Anything else that cannot be represented here - including invalid input. * Since it cannot be parsed, the get-Methods mostly do not provide a value here, except {@link #getUrl()}. */ OTHER } /** * Marker for {@link #getScheme()} for a protocol relative URL, e.g. //www/something.css - just the empty string, as opposed to null for no scheme as in {@link UrlType#RELATIVE}. */ public static final String SCHEME_PROTOCOL_RELATIVE_URL = ""; protected static final Pattern SCHEME_PATTERN = Pattern.compile("(?i:(?[a-zA-Z0-9+.-]+):)"); protected static final Pattern HTTP_SCHEME_PATTERN = Pattern.compile("(?i:(?https?):)"); protected static final Pattern FILE_SCHEME_PATTERN = Pattern.compile("(?i:(?(file|ftp)):)"); protected static final Pattern USERNAMEPASSWORD = Pattern.compile("((?[^:@/?#]+)(:(?[^:@/?#]+))?@)"); /** * Regex matching various variants of Sling-URLs. *

* Debug regex e.g. with http://www.softlion.com/webtools/regexptest/ . */ protected static final Pattern HTTP_URL_PATTERN = Pattern.compile("^" + HTTP_SCHEME_PATTERN.pattern() + "?" + "(?//" + USERNAMEPASSWORD + "?((?[^/:?#]+)(:(?[0-9]+))?)?)?" + "(?(/([^/.;?#]+|\\.\\.))*/)" + "(" + "(?[^/.?#;]+)" + "((?(\\.[^./;?#]+)+)(?/[^;?#]*)?)?" + // A suffix can only exist if there is an extension. ")?" + "(?\\?[^#]*)?" + "(?#.*)?" + "$" ); protected static final Pattern FILE_URL_PATTERN = Pattern.compile("^" + FILE_SCHEME_PATTERN.pattern() + "?" + "(?//" + USERNAMEPASSWORD + "?((?[^/:?#]+)(:(?[0-9]+))?)?)?" + "(?(/([^/.;?#]+|\\.\\.))*/)" + "(" + "(?[^/.;?#]+)" + "((?(\\.[^./;?#]+)+)(?/[^;?#]*)?)?" + ")?" + "(?)(?)" + // empty groups to allow easier reading out of the matcher "$" ); protected static final Pattern ABSOLUTE_PATH_PATTERN = Pattern.compile("^" + "(?(/([^/.?]+|\\.\\.))*/)" + "(" + "(?[^/.;?#]+)" + "((?(\\.[^./;?#]+)+)(?/[^;?#]*)?)?" + ")?" + "(?\\?[^#]*)?" + "(?#.*)?" + "$" ); protected static final Pattern RELATIVE_PATH_PATTERN = Pattern.compile("^" + "(?([^/.;?#]+|\\.\\.)*/)?" + "(" + "(?[^/.;?#]+)" + "((?(\\.[^./;?#]+)+)(?/[^;?#]*)?)?" + ")?" + "(?\\?[^#]*)?" + "(?#.*)?" + "$" ); protected static final Pattern HTTP_SCHEME = Pattern.compile("^https?$", Pattern.CASE_INSENSITIVE); protected static final Pattern FILE_SCHEME = Pattern.compile("^(file|ftp)$", Pattern.CASE_INSENSITIVE); protected static final Pattern SPECIAL_SCHEME = Pattern.compile("^(mailto|tel|fax)$", Pattern.CASE_INSENSITIVE); protected UrlType type; /** * The scheme of the URL. To be able to represent protocol-relative URL, we distinguish here null and the empty string: * null is no scheme, empty string is a protocol-relative URL (e.g. //www/something.css). */ protected String scheme; protected String username; protected String password; protected String host; protected Integer port; protected String contextPath; /** * Contains the path inclusive leading and trailing / , not the file/resource itself: for /a/b/c it's /a/b/ . * Emptly for relative paths / unparseable stuff. */ protected String path; /** * The filename; if the url could not be parsed ({@link UrlType#SPECIAL} or {@link UrlType#OTHER}), this contains the url without the scheme. */ protected String name; protected final List selectors = new ArrayList<>(); protected String extension; protected String suffix; protected final LinkedHashMap> parameters = new LinkedHashMap<>(); protected String fragment; private transient String url; protected transient String resourcePath; protected transient Resource resource; protected final SlingHttpServletRequest request; protected final LinkMapper linkMapper; public SlingUrl(@NotNull final SlingHttpServletRequest request, @NotNull final Resource resource) { this(request, resource, null, null); } public SlingUrl(@NotNull final SlingHttpServletRequest request, @NotNull final Resource resource, @Nullable final String extension) { this(request, resource, null, extension, null, null, true, getLinkMapper(request, null)); } public SlingUrl(@NotNull final SlingHttpServletRequest request, @NotNull final Resource resource, @Nullable final String selectors, @Nullable final String extension) { this(request, resource, selectors, extension, null, null, true, getLinkMapper(request, null)); } public SlingUrl(@NotNull final SlingHttpServletRequest request, @NotNull final Resource resource, @Nullable final String selectors, @Nullable final String extension, @Nullable final String suffix) { this(request, resource, selectors, extension, suffix, null, true, getLinkMapper(request, null)); } public SlingUrl(@NotNull final SlingHttpServletRequest request, @NotNull final Resource resource, @Nullable final String selectors, @Nullable final String extension, @Nullable final String suffix, @Nullable final String parameterString) { this(request, resource, selectors, extension, suffix, parameterString, true, getLinkMapper(request, null)); } public SlingUrl(@NotNull final SlingHttpServletRequest request, @NotNull final Resource resource, @Nullable final String selectors, @Nullable final String extension, @Nullable final String suffix, @Nullable final String parameterString, boolean decodeParameters, @Nullable LinkMapper linkMapper) { this.request = Objects.requireNonNull(request, "request required"); this.linkMapper = linkMapper; this.resource = resource; this.resourcePath = resource.getPath(); String escapedResourcePath = LinkUtil.namespacePrefixEscape(resourcePath); this.name = StringUtils.substringAfterLast(escapedResourcePath, "/"); this.path = escapedResourcePath.substring(0, escapedResourcePath.length() - name.length()); this.type = UrlType.HTTP; setSelectors(selectors); setExtension(extension); setSuffix(suffix); setParameters(parameterString, decodeParameters); } /** * Constructs a yet invalid SlingUrl that has to be initialized with one of the from* methods. * A linkmapper can be given if the url is a path. */ public SlingUrl(@NotNull final SlingHttpServletRequest request, @Nullable LinkMapper linkMapper) { this.request = Objects.requireNonNull(request, "request required"); this.linkMapper = linkMapper; } /** * Constructs a yet invalid SlingUrl that has to be initialized with one of the from* methods. */ public SlingUrl(@NotNull final SlingHttpServletRequest request) { this(request, getLinkMapper(request, null)); } /** * @deprecated use new SlingUrl(request).fromUrl(url) */ // FIXME(hps,23.06.20) remove after merging and adapting other projects @Deprecated public SlingUrl(@NotNull final SlingHttpServletRequest request, String url) { this(request); fromUrl(url); } /** * Parses the url. * Caution: if the url contains several periods like e.g. http://host/a.b/c.d/suffix , this * might parse it wrong, since we'd have to consult the resource tree to determine whether the resource path * is /a or /a.b/c . * Caution 2: * This applies URL-decoding that replaces percent encoded characters (%[0-9a-fA-F][0-9a-fA-F]) by their decoded counterparts * before saving the URL (except if it'll be of type {@link UrlType#OTHER}, where we don't touch anything). * If there are characters outside the URL range (say, the user types an url http://www/päth) these are left untouched for now. * When the URL is reconstructed in {@link #getUrl()}, such characters are encoded (to http://www/p%C3A4th). * This might however lead to some rare trouble when there is something in there that looks like a percent encoded character * but isn't meant as such, e.g. '%effect', but that won't work right in a browser, too. * * @return this for builder style chaining */ @NotNull public SlingUrl fromUrl(@NotNull final String url) { return fromUrl(url, true); } /** * Parses the url, possibly URL-decoding it when decode = true. * Caution: if the url contains several periods like e.g. http://host/a.b/c.d/suffix , this * might parse it wrong, since we'd have to consult the resource tree to determine whether the resource path * is /a or /a.b/c . */ @NotNull public SlingUrl fromUrl(@NotNull final String url, boolean decode) { parseUrl(url, decode); return this; } /** * Assigns from the given uri, trying to parse extension and suffix from it. * This avoids any issues with character encoding and broken URLs. * Caution: if the url contains several periods like e.g. http://host/a.b/c.d/suffix , this * * might parse it wrong, since we'd have to consult the resource tree to determine whether the resource path * * is /a or /a.b/c . */ public SlingUrl fromUri(@NotNull URI uri) { reset(); boolean isHttp = uri.getScheme() != null && HTTP_SCHEME.matcher(uri.getScheme()).matches(); boolean isFile = uri.getScheme() != null && FILE_SCHEME.matcher(uri.getScheme()).matches(); boolean isSpecial = uri.getScheme() != null && SPECIAL_SCHEME.matcher(uri.getScheme()).matches(); if (isSpecial) { this.type = UrlType.SPECIAL; this.scheme = uri.getScheme(); this.name = uri.getSchemeSpecificPart(); if (uri.getFragment() != null) { this.name = this.name + "#" + uri.getFragment(); } } else if (uri.isOpaque() || isNotBlank(uri.getScheme()) && !isFile && !isHttp) { this.type = UrlType.OTHER; this.scheme = uri.getScheme(); this.name = uri.getRawSchemeSpecificPart(); if (uri.getRawFragment() != null) { this.name = this.name + "#" + uri.getRawFragment(); } } else { // first we parse the path since that might cause a reset Matcher matcher = ABSOLUTE_PATH_PATTERN.matcher(uri.getRawPath()); if (matcher.matches()) { assignFromGroups(matcher, true, false); } else if ((matcher = RELATIVE_PATH_PATTERN.matcher(uri.getRawPath())).matches()) { assignFromGroups(matcher, true, false); } else { // unparseable path - fall back to OTHER :-( reset(); this.type = UrlType.OTHER; this.name = uri.getRawSchemeSpecificPart(); this.fragment = StringUtils.defaultIfBlank(uri.getRawFragment(), null); } this.scheme = uri.getScheme(); if (this.scheme == null && StringUtils.isNotBlank(uri.getHost())) { this.scheme = SCHEME_PROTOCOL_RELATIVE_URL; } if (type != UrlType.OTHER) { this.host = uri.getHost(); if (uri.getPort() >= 0) { this.port = uri.getPort(); } if (scheme == null && !StringUtils.startsWith(uri.getSchemeSpecificPart(), "/")) { this.type = UrlType.RELATIVE; } else { this.type = isFile ? UrlType.FILE : UrlType.HTTP; } if (uri.getUserInfo() != null) { // this fails if username contains an encoded : this.username = StringUtils.defaultIfBlank(StringUtils.substringBefore(uri.getUserInfo(), ":"), null); this.password = StringUtils.defaultIfBlank(StringUtils.substringAfter(uri.getUserInfo(), ":"), null); } this.fragment = uri.getFragment(); if (uri.getRawQuery() != null) { parseParameters(uri.getRawQuery(), true); } } } return this; } /** * Initializes from an url or an absolute resource path, possibly URL-decoding it when decode = true. * If pathOrUrl starts with a single slash, this is the same as {@link #fromPath(String)}, otherwise the same as * {@link #fromUrl(String, boolean)}. * * @return this for builder style chaining */ public SlingUrl fromPathOrUrl(@NotNull final String pathOrUrl, boolean decode) { if (isAbsolutePath(pathOrUrl)) return fromPath(pathOrUrl); else return fromUrl(pathOrUrl, true); } /** * Initializes from an url or an absolute resource path. * If pathOrUrl starts with a single slash, this is the same as {@link #fromPath(String)}, otherwise the same as * {@link #fromUrl(String)}. * * @return this for builder style chaining */ public SlingUrl fromPathOrUrl(@NotNull final String pathOrUrl) { if (isAbsolutePath(pathOrUrl)) return fromPath(pathOrUrl); else return fromUrl(pathOrUrl, true); } protected boolean isAbsolutePath(String pathOrUrl) { return StringUtils.startsWith(pathOrUrl, "/") && !StringUtils.startsWith(pathOrUrl, "//"); // not a protocol relative url } /** * Initializes the SlingUrl from a resource path, absolute or relative. * * @param resourcePath an absolute or relative path * @return this for builder style chaining */ @NotNull public SlingUrl fromPath(@NotNull String resourcePath) { reset(); type = resourcePath.startsWith("/") ? UrlType.HTTP : UrlType.RELATIVE; setResourcePath(resourcePath); return this; } /** * Initialzes the SlingUrl from a requests URL * * @param request the HTTP request to use * @return this for builder style chaining */ @NotNull public SlingUrl fromRequest(HttpServletRequest request) { StringBuffer url = request.getRequestURL(); String query = request.getQueryString(); if (StringUtils.isNotBlank(query)) { url.append('?').append(query); } return fromUrl(url.toString(), true); } @NotNull public SlingHttpServletRequest getRequest() { return request; } /** * An external URL: we assume it's external when a scheme is set. */ public boolean isExternal() { return scheme != null; } /** * For internal urls the path to the rendered resource. The path to {@link #getResource()} if that exists. */ @Nullable public String getResourcePath() { if (resourcePath == null && !isExternal() && type == UrlType.HTTP) { ResourceResolver resolver = request.getResourceResolver(); String candidateResourcePath = LinkUtil.namespacePrefixUnescape(defaultString(path) + defaultString(name)); this.resourcePath = candidateResourcePath; if (isNotBlank(extension)) { this.resourcePath += '.' + extension; } resource = resolver.getResource(this.resourcePath); if (resource == null) { this.resourcePath = candidateResourcePath; resource = resolver.getResource(this.resourcePath); } } return resourcePath; } /** * For internal urls: if this is a path to a resource, this returns it (suffix, selectors and parameters ignored, if present). */ @Nullable public Resource getResource() { getResourcePath(); // possibly initialize return resource; } /** * If an internal path starts with the {@link HttpServletRequest#getContextPath()}, this contains the context path, * and the context path is removed from {@link #getPathAndName()}. */ @Nullable public String getContextPath() { return isNotBlank(contextPath) ? contextPath : request.getContextPath(); } /** * The path to the file including the filename, but not the extension, selectors etc. */ @NotNull public String getPathAndName() { return defaultString(path) + defaultString(name); } /** * The path to the file including the filename and the extension, but no selectors. */ @NotNull public String getPathAndNameExt() { return defaultString(path) + defaultString(name) + (isNotBlank(extension) ? "." + extension : ""); } /** * Sets the path, name and extension from the given e.g. resource path. * Caution: if the fullpath contains several periods, the result can be different than you expect and broken. * * @return this for builder style chaining */ public SlingUrl setPathAndNameExt(@Nullable String fullpath) { this.path = null; this.name = null; this.extension = null; clearTransients(); if (isNotBlank(fullpath)) { if (type == null) { type = fullpath.startsWith("/") ? UrlType.HTTP : UrlType.RELATIVE; } int endpath = fullpath.lastIndexOf('/'); String newPath = null; String newName = null; String newExtension = null; if (endpath >= 0) { newPath = fullpath.substring(0, endpath + 1); fullpath = fullpath.substring(endpath + 1); } int period = fullpath.lastIndexOf('.'); if (period >= 0) { newName = fullpath.substring(0, period); newExtension = fullpath.substring(period + 1); } this.path = newPath; this.name = newName; this.extension = newExtension; } return this; } /** * Sets the path and name from the given e.g. resource path, but does not touch the {@link #getExtension()}. * Alias for {@link #setResourcePath(String)} * * @param fullpath the path and filename to be set. We do not check for periods in there. * @return this for builder style chaining */ public SlingUrl setPathAndName(@Nullable String fullpath) { return setResourcePath(resourcePath); } /** * Sets the resource path encoded in the URL to the given path. This touches path and {@link #getName()}, * but not selectors and extension, works also when the resource path contains periods. * * @param resourcePath an absolute or possibly relative path. We do not check for periods in there. * @return this for builder style chaining */ public SlingUrl setResourcePath(@Nullable String resourcePath) { this.path = null; this.name = null; clearTransients(); if (isNotBlank(resourcePath)) { String escapedPath = LinkUtil.namespacePrefixEscape(resourcePath); int endpath = escapedPath.lastIndexOf('/'); String newPath = null; String newName = escapedPath; if (endpath >= 0) { newPath = escapedPath.substring(0, endpath + 1); newName = escapedPath.substring(endpath + 1); } this.path = newPath; this.name = newName; } return this; } /** * Sets the resource path encoded in the URL to the given path. This touches path and {@link #getName()}, * but not selectors and extension, works also when the resource path contains periods. * Alias for {@link #setResourcePath(String)}. * * @param resourcePath an absolute or possibly relative path. We do not check for periods in there. * @return this for builder style chaining */ public SlingUrl resourcePath(@Nullable String resourcePath) { return setResourcePath(resourcePath); } @NotNull List getSelectors() { return selectors; } public SlingUrl selector(@Nullable String... value) { return addSelector(value); } public SlingUrl addSelector(@Nullable String... value) { clearTransients(); if (value != null) { selectors.addAll(Arrays.asList(value)); } return this; } public SlingUrl setSelector(String... value) { clearSelectors(); addSelector(value); return this; } public SlingUrl selectors(@Nullable final String selectors) { return setSelectors(selectors); } /** * Sets the selectors to the given string - can contain multiple selectors separated with period. */ public SlingUrl setSelectors(@Nullable final String selectors) { clearSelectors(); if (selectors != null) { for (String sel : StringUtils.split(selectors, '.')) { if (isNotBlank(sel)) { this.selectors.add(sel); } } } return this; } public SlingUrl removeSelector(String... value) { clearTransients(); if (value != null) { for (String val : value) { selectors.remove(val); } } return this; } public SlingUrl clearSelectors() { clearTransients(); selectors.clear(); return this; } @Nullable public String getExtension() { return extension; } public SlingUrl extension(@Nullable final String extension) { return setExtension(extension); } public SlingUrl setExtension(@Nullable String extension) { if (extension != null) { int dot = extension.lastIndexOf("."); if (dot >= 0) { extension = extension.substring(dot + 1); } } this.extension = extension; clearTransients(); return this; } @Nullable public String getSuffix() { return suffix; } public SlingUrl suffix(@Nullable final Resource resource) { return setSuffix(resource != null ? resource.getPath() : null); } public SlingUrl suffix(@Nullable final Resource resource, @Nullable final String extension) { return setSuffix(resource != null ? resource.getPath() + (isNotBlank(extension) ? ("." + extension) : "") : null); } public SlingUrl suffix(@Nullable final String suffix) { return setSuffix(suffix); } public SlingUrl setSuffix(@Nullable final String suffix) { this.suffix = suffix; clearTransients(); return this; } /** * @return the first value of the parameter values if values are present; * an empty string if the value list is empty (parameter present but without a value); * null if the parameter is not present */ @Nullable public String getParameter(@NotNull final String name) { List values = parameters.get(name); return values != null ? (values.size() > 0 ? values.get(0) : "") : null; } /** * @return an unmodifiable List of the parameter values if present */ @Nullable public List getParameterValues(@NotNull final String name) { List values = parameters.get(name); return values != null ? Collections.unmodifiableList(values) : null; } /** * Unmodifiable map of the contained parameters. *

* This is unmodifiable since otherwise we would have to trust the user to call {@link #clearTransients()} on every change. */ @NotNull public Map> getParameters() { return Collections.unmodifiableMap(parameters); } public SlingUrl parameter(String name, String... value) { return addParameter(name, value); } public SlingUrl addParameter(String name, String... value) { clearTransients(); List values = parameters.computeIfAbsent(name, k -> new ArrayList<>()); if (value != null) { for (String val : value) { if (val != null) { values.add(val); } } } return this; } /** * Adds parameters parsed from an HTTP query string. */ public SlingUrl addParameters(String parameterString, boolean decode) { if (parameterString != null) { parseParameters(parameterString, decode); } return this; } public SlingUrl setParameter(String name, String... value) { removeParameter(name); addParameter(name, value); return this; } public SlingUrl parameters(String parameterString) { return setParameters(parameterString, true); } public SlingUrl setParameters(String parameterString, boolean decode) { clearParameters(); addParameters(parameterString, decode); return this; } public SlingUrl removeParameter(String name) { clearTransients(); parameters.remove(name); return this; } public SlingUrl clearParameters() { clearTransients(); parameters.clear(); return this; } /** * The fragment part of the URL. Caution: this isn't available on all types. */ @Nullable public String getFragment() { return fragment; } /** * Sets the fragment. Caution: this isn't available on all types. * Same as {@link #fragment(String)}. * * @return this for builder style chaining */ @NotNull public SlingUrl setFragment(@Nullable String fragment) { return fragment(fragment); } /** * Sets the fragment part of the URL. Caution: this isn't available on all types. * Same as {@link #setFragment(String)}. * * @return this for builder style chaining */ @NotNull public SlingUrl fragment(@Nullable String fragment) { this.fragment = fragment; clearTransients(); return this; } @Override public int hashCode() { return toString().hashCode(); } /** * Just compares {@link #toString()} - that is, the url. */ @SuppressWarnings("EqualsWhichDoesntCheckParameterClass") @Override public boolean equals(Object other) { return other != null && toString().equals(other.toString()); } /** * Same as {@link #getUrl()}. */ @Override public String toString() { return getUrl(); } /** * Builds the URL from its parts saved in the builder. */ public String getUrl() { if (url == null) { url = buildUrl(); } return url; } /** * Internal reset method for the calculated transient values like the URL, called when anything changes. */ protected void clearTransients() { this.url = null; this.resource = null; this.resourcePath = null; } /** * The scheme part of an URL. Caution: this isn't available on all types. * To be able to represent protocol-relative URL, we distinguish here null and the empty string: * null is no scheme applicable, as in {@link UrlType#RELATIVE}, * empty string is a protocol-relative URL (e.g. //www/something.css). */ @Nullable public String getScheme() { return scheme; } /** * Sets the scheme. Caution: this isn't available on all types. * To be able to represent protocol-relative URL, we distinguish here null and the empty string: * null is no scheme, empty string is a protocol-relative URL (e.g. //www/something.css). * Same as {@link #scheme(String)}. * * @return this for builder style chaining */ @NotNull public SlingUrl setScheme(@Nullable String scheme) { return scheme(scheme); } /** * Sets the scheme part of an URL. Caution: this isn't available on all types. * Same as {@link #setScheme(String)}. * * @return this for builder style chaining */ @NotNull public SlingUrl scheme(@Nullable String scheme) { this.scheme = scheme; clearTransients(); return this; } /** * The username part of an URL. Caution: this isn't available on all types. */ @Nullable public String getUsername() { return username; } /** * Sets the username. Caution: this isn't available on all types. * Same as {@link #username(String)}. * * @return this for builder style chaining */ @NotNull public SlingUrl setUsername(@Nullable String username) { return username(username); } /** * Sets the username part of an URL. Caution: this isn't available on all types. * Same as {@link #setUsername(String)}. * * @return this for builder style chaining */ @NotNull public SlingUrl username(@Nullable String username) { this.username = username; clearTransients(); return this; } /** * The password part of an URL. Caution: this isn't available on all types. */ @Nullable public String getPassword() { return password; } /** * Sets the password. Caution: this isn't available on all types. * Same as {@link #password(String)}. * * @return this for builder style chaining */ @NotNull public SlingUrl setPassword(@Nullable String password) { return password(password); } /** * Sets the password part of an URL. Caution: this isn't available on all types. * Same as {@link #setPassword(String)}. * * @return this for builder style chaining */ @NotNull public SlingUrl password(@Nullable String password) { this.password = password; clearTransients(); return this; } /** * The host part of the URL. Caution: this isn't available on all types. */ @Nullable public String getHost() { return host; } /** * Sets the host part of the URL. Caution: this isn't available on all types. * Same as {@link #host(String)}. * * @return this for builder style chaining */ @NotNull public SlingUrl setHost(@Nullable String host) { return host(host); } /** * Sets the host part of the URL. Caution: this isn't available on all types. * Same as {@link #setHost(String)}. * * @return this for builder style chaining */ @NotNull public SlingUrl host(@Nullable String host) { this.host = host; clearTransients(); return this; } /** * The port part of the URL. Caution: this isn't available on all types. */ @Nullable public Integer getPort() { return port; } /** * Sets the port. Caution: this isn't available on all types. * Same as {@link #port(Integer)}. * * @return this for builder style chaining */ @NotNull public SlingUrl setPort(@Nullable Integer port) { return port(port); } /** * Sets the port part of the URL. Caution: this isn't available on all types. * Same as {@link #setPort(Integer)}. * * @return this for builder style chaining */ @NotNull public SlingUrl port(@Nullable Integer port) { this.port = port; clearTransients(); return this; } /** * The type of the URL. Caution: this isn't available on all types. */ @NotNull public UrlType getType() { return type; } /** * Sets the type. Caution: this isn't available on all types. * Same as {@link #type(UrlType)}. * * @return this for builder style chaining */ @NotNull public SlingUrl setType(@NotNull UrlType type) { return type(type); } /** * Sets the type of the URL. Caution: this isn't available on all types. * Same as {@link #setType(UrlType)}. * * @return this for builder style chaining */ @NotNull public SlingUrl type(@NotNull UrlType type) { this.type = Objects.requireNonNull(type); clearTransients(); return this; } /** * The filename; in case of type OTHER this contains the whole URL but the scheme and colon. Caution: this isn't available on all types. */ @Nullable public String getName() { return name; } /** * Sets the name. Caution: this isn't available on all types. * Same as {@link #name(String)}. * * @return this for builder style chaining */ @NotNull public SlingUrl setName(@Nullable String name) { return name(name); } /** * Sets the filename; in case of type OTHER this contains the whole URL but the scheme and colon. Caution: this isn't available on all types. * Same as {@link #setName(String)}. * * @return this for builder style chaining */ @NotNull public SlingUrl name(@Nullable String name) { this.name = name; clearTransients(); return this; } @Override public SlingUrl clone() { try { return (SlingUrl) super.clone(); } catch (CloneNotSupportedException e) { // Should be impossible. throw new IllegalArgumentException("Bug: clone threw error for " + getClass().toString(), e); } } public void reset() { type = null; scheme = null; host = null; port = null; contextPath = null; path = null; name = null; selectors.clear(); extension = null; suffix = null; parameters.clear(); fragment = null; clearTransients(); resourcePath = null; resource = null; } protected String buildUrl() { StringBuilder builder = new StringBuilder(); if (isNotBlank(scheme)) { builder.append(scheme).append(':'); } if (type == null) { LOG.error("NO TYPE! {}", this.toDebugString()); } else if (type == UrlType.OTHER) { builder.append(name); // do not encode anything in OTHER - that can trash meta-characters } else if (type == UrlType.SPECIAL) { builder.append(UrlCodec.OPAQUE.encode(name)); // weakest possible codec } else { if (scheme != null) { // scheme="" is a protocol relative URL starting with // . builder.append("//"); } if (isNotBlank(host)) { if (isNotBlank(username)) { builder.append(UrlCodec.AUTHORITY.encode(username)); if (isNotBlank(password)) { builder.append(":").append(UrlCodec.AUTHORITY.encode(password)); } builder.append("@"); } builder.append(UrlCodec.AUTHORITY.encode(host)); if (port != null) { builder.append(":").append(port); } } String pathAndName = defaultString(path) + defaultString(name); if (isExternal()) { pathAndName = UrlCodec.PATH.encode(pathAndName); } else if (linkMapper != null && type != UrlType.RELATIVE) { pathAndName = linkMapper.mapUri(request, LinkUtil.namespacePrefixUnescape(pathAndName)); pathAndName = adjustMappedUrl(request, pathAndName); } else { pathAndName = LinkUtil.encodePath(pathAndName); } builder.append(pathAndName); for (String value : selectors) { builder.append('.').append(UrlCodec.PATH.encode(value)); } if (isNotBlank(extension)) { builder.append('.').append(UrlCodec.PATH.encode(extension)); } if (isNotBlank(suffix)) { builder.append(isExternal() ? UrlCodec.PATH.encode(suffix) : LinkUtil.encodePath(suffix)); } if (parameters.size() > 0) { int index = 0; for (Map.Entry> param : parameters.entrySet()) { String paramName = param.getKey(); List values = param.getValue(); if (values.size() > 0) { for (String val : values) { builder.append(index == 0 ? '?' : '&'); builder.append(UrlCodec.QUERYPART.encode(paramName)); if (val != null) { builder.append("=").append(UrlCodec.QUERYPART.encode(val)); } index++; } } else { builder.append(index == 0 ? '?' : '&'); builder.append(UrlCodec.QUERYPART.encode(paramName)); index++; } } } if (isNotBlank(fragment)) { builder.append('#').append(UrlCodec.FRAGMENT.encode(fragment)); } } return builder.toString(); } protected void parseUrl(@NotNull final String url, final boolean decode) throws IllegalArgumentException { reset(); Matcher schemeMatcher = SCHEME_PATTERN.matcher(url); int schemeLength = 0; if (schemeMatcher.lookingAt()) { scheme = schemeMatcher.group("scheme"); schemeLength = schemeMatcher.end(); } else if (url.startsWith("//")) { scheme = SCHEME_PROTOCOL_RELATIVE_URL; // special marker for protocol relative URL schemeLength = 0; } boolean other = false; if (scheme != null) { if (HTTP_SCHEME.matcher(scheme).matches() || SCHEME_PROTOCOL_RELATIVE_URL.equals(scheme)) { Matcher matcher = HTTP_URL_PATTERN.matcher(url); if (matcher.matches()) { // normal URL type = UrlType.HTTP; assignFromGroups(matcher, decode, true); } else { // doesn't match URL_PATTERN, can't parse -> other other = true; } } else if (FILE_SCHEME.matcher(scheme).matches()) { Matcher matcher = FILE_URL_PATTERN.matcher(url); if (matcher.matches()) { // normal URL type = UrlType.FILE; assignFromGroups(matcher, decode, true); } else { // doesn't match URL_PATTERN, can't parse -> other other = true; } } else if (SPECIAL_SCHEME.matcher(scheme).matches()) { // mailto, tel, ... - unprocessed type = UrlType.SPECIAL; name = decode(url.substring(schemeMatcher.end()), decode); } else { // non-special scheme other = true; } } else { // no scheme : path or other Matcher matcher; if ((matcher = ABSOLUTE_PATH_PATTERN.matcher(url)).matches()) { type = UrlType.HTTP; // it'll turn into HTTP on getUrl(). assignFromGroups(matcher, decode, false); } else if ((matcher = RELATIVE_PATH_PATTERN.matcher(url)).matches()) { type = UrlType.RELATIVE; assignFromGroups(matcher, decode, false); } else { // doesn't match URL_PATTERN, can't parse -> other other = true; } } if (other) { type = UrlType.OTHER; // do not decode anything in OTHER since that can trash meta-characters name = url.substring(schemeLength); } } protected void assignFromGroups(Matcher matcher, boolean decode, boolean hostAndPort) { String value; if (hostAndPort) { if (isNotBlank(value = matcher.group("host"))) { host = value; } if (isNotBlank(value = matcher.group("port"))) { port = Integer.parseInt(value); } if (isNotBlank(value = matcher.group("username"))) { username = value; } if (isNotBlank(value = matcher.group("password"))) { password = value; } } if (isNotBlank(value = matcher.group("pathnoext"))) { path = decode(value, decode); String contextPath = request.getContextPath(); if (isNotBlank(contextPath) && path.startsWith(contextPath + "/")) { this.contextPath = contextPath; path = path.substring(contextPath.length()); } } name = isNotBlank(value = matcher.group("filenoext")) ? decode(value, decode) : null; if (isNotBlank(value = matcher.group("extensions"))) { String[] selExt = StringUtils.split(value.substring(1), '.'); for (int i = 0; i < selExt.length - 1; i++) { selectors.add(decode(selExt[i], decode)); } extension = decode(selExt[selExt.length - 1], decode); } if (isNotBlank(value = matcher.group("suffix"))) { suffix = decode(value, decode); } if (isNotBlank(value = matcher.group("query"))) { parseParameters(value, decode); } if (isNotBlank(value = matcher.group("fragment"))) { fragment = decode(value, decode).substring(1); } } protected String decode(String value, boolean decode) { // here any decoder is OK, since we just want to resolve percent encodings return decode ? UrlCodec.URLSAFE.decode(value) : value; } protected void parseParameters(@NotNull String parameterString, boolean decode) { parameterString = parameterString.trim(); while (parameterString.startsWith("?")) { parameterString = parameterString.substring(1); } for (String param : StringUtils.split(parameterString, '&')) { int delimiterPos = param.indexOf('='); String name = delimiterPos < 0 ? param : param.substring(0, delimiterPos); String value = delimiterPos < 0 ? null : param.substring(delimiterPos + 1); addParameter(decode ? UrlCodec.QUERYPART.decode(name) : name, value != null ? (decode ? UrlCodec.QUERYPART.decode(value) : value) : null); } } protected static LinkMapper getLinkMapper(@NotNull final SlingHttpServletRequest request, @Nullable LinkMapper linkMapper) { if (linkMapper == null) { linkMapper = (LinkMapper) request.getAttribute(LinkMapper.LINK_MAPPER_REQUEST_ATTRIBUTE); if (linkMapper == null) { linkMapper = LinkMapper.RESOLVER; } } return linkMapper; } /** * Lists the internal parse results - mainly for debugging purposes. */ public String toDebugString() { ToStringBuilder builder = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE); if (type != null) { builder.append("type", type); } if (scheme != null) { builder.append("scheme", scheme); } if (username != null) { builder.append("username", username); } if (password != null) { builder.append("password", password); } if (host != null) { builder.append("host", host); } if (port != null) { builder.append("port", port); } // deliberately no context path since that's calculated on demand if (path != null) { builder.append("path", path); } if (name != null) { builder.append("name", name); } if (!selectors.isEmpty()) { builder.append("selectors", selectors); } if (extension != null) { builder.append("extension", extension); } if (suffix != null) { builder.append("suffix", suffix); } if (!parameters.isEmpty()) { builder.append("parameters", parameters); } if (fragment != null) { builder.append("fragment", fragment); } if (isExternal()) { builder.append("external", true); } if (getResourcePath() != null) { builder.append("resourcePath", resourcePath); } return builder.toString(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy