| /* |
| * 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.api.uri; |
| |
| import java.io.UnsupportedEncodingException; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.net.URLEncoder; |
| import java.nio.charset.StandardCharsets; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.NoSuchElementException; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import org.apache.sling.api.SlingHttpServletRequest; |
| import org.apache.sling.api.request.RequestPathInfo; |
| import org.apache.sling.api.resource.Resource; |
| import org.apache.sling.api.resource.ResourceResolver; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import org.osgi.annotation.versioning.ProviderType; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * Builder for SlingUris that allows to set any properties of a {@link SlingUri}. |
| * <p> |
| * Example: |
| * |
| * <pre> |
| * SlingUri testUri = SlingUriBuilder.create() |
| * .setResourcePath("/test/to/path") |
| * .setSelectors(new String[] { "sel1", "sel2" }) |
| * .setExtension("html") |
| * .setSuffix("/suffix/path") |
| * .setQuery("par1=val1&par2=val2") |
| * .build(); |
| * </pre> |
| * <p> |
| * |
| * @since 1.0.0 (Sling API Bundle 2.23.0) |
| */ |
| @ProviderType |
| public class SlingUriBuilder { |
| private static final Logger LOG = LoggerFactory.getLogger(SlingUriBuilder.class); |
| |
| private static final String HTTPS_SCHEME = "https"; |
| private static final int HTTPS_DEFAULT_PORT = 443; |
| private static final String HTTP_SCHEME = "http"; |
| private static final int HTTP_DEFAULT_PORT = 80; |
| private static final String FILE_SCHEME = "file"; |
| |
| static final String CHAR_HASH = "#"; |
| static final String CHAR_QM = "?"; |
| static final char CHAR_AMP = '&'; |
| static final char CHAR_AT = '@'; |
| static final char CHAR_SEMICOLON = ';'; |
| static final char CHAR_EQUALS = '='; |
| static final char CHAR_SINGLEQUOTE = '\''; |
| static final String CHAR_COLON = ":"; |
| static final String CHAR_DOT = "."; |
| static final String CHAR_SLASH = "/"; |
| static final String SELECTOR_DOT_REGEX = "\\.(?!\\.?/)"; // (?!\\.?/) to avoid matching ./ and ../ |
| static final String PATH_PARAMETERS_REGEX = ";([a-zA-z0-9]+)=(?:\\'([^']*)\\'|([^/]+))"; |
| static final String BEST_EFFORT_INVALID_URI_MATCHER = "^(?:([^:#@]+):)?(?://(?:([^@#]+)@)?([^/#:]+)(?::([0-9]+))?)?(?:([^?#]+))?(?:\\?([^#]*))?(?:#(.*))?$"; |
| |
| /** |
| * Creates a builder without any URI parameters set. |
| * |
| * @return a SlingUriBuilder |
| */ |
| @NotNull |
| public static SlingUriBuilder create() { |
| return new SlingUriBuilder(); |
| } |
| |
| /** |
| * Creates a builder from another SlingUri (clone and modify use case). |
| * |
| * @param slingUri the Sling URI to clone |
| * @return a SlingUriBuilder |
| */ |
| @NotNull |
| public static SlingUriBuilder createFrom(@NotNull SlingUri slingUri) { |
| return create() |
| .setScheme(slingUri.getScheme()) |
| .setUserInfo(slingUri.getUserInfo()) |
| .setHost(slingUri.getHost()) |
| .setPort(slingUri.getPort()) |
| .setResourcePath(slingUri.getResourcePath()) |
| .setPathParameters(slingUri.getPathParameters()) |
| .setSelectors(slingUri.getSelectors()) |
| .setExtension(slingUri.getExtension()) |
| .setSuffix(slingUri.getSuffix()) |
| .setQuery(slingUri.getQuery()) |
| .setFragment(slingUri.getFragment()) |
| .setSchemeSpecificPart(slingUri.isOpaque() ? slingUri.getSchemeSpecificPart() : null) |
| .setResourceResolver(slingUri instanceof ImmutableSlingUri |
| ? ((ImmutableSlingUri) slingUri).getData().resourceResolver |
| : null); |
| } |
| |
| /** |
| * Creates a builder from a resource (only taking the resource path into account). |
| * |
| * @param resource the resource to take the resource path from |
| * @return a SlingUriBuilder |
| */ |
| @NotNull |
| public static SlingUriBuilder createFrom(@NotNull Resource resource) { |
| return create() |
| .setResourcePath(resource.getPath()) |
| .setResourceResolver(resource.getResourceResolver()); |
| } |
| |
| /** |
| * Creates a builder from a RequestPathInfo instance . |
| * |
| * @param requestPathInfo the request path info to take resource path, selectors, extension and suffix from. |
| * @return a SlingUriBuilder |
| */ |
| @NotNull |
| public static SlingUriBuilder createFrom(@NotNull RequestPathInfo requestPathInfo) { |
| Resource suffixResource = requestPathInfo.getSuffixResource(); |
| return create() |
| .setResourceResolver(suffixResource != null ? suffixResource.getResourceResolver() : null) |
| .setResourcePath(requestPathInfo.getResourcePath()) |
| .setSelectors(requestPathInfo.getSelectors()) |
| .setExtension(requestPathInfo.getExtension()) |
| .setSuffix(requestPathInfo.getSuffix()); |
| } |
| |
| /** |
| * Creates a builder from a request. |
| * |
| * @param request request to take the URI information from |
| * @return a SlingUriBuilder |
| */ |
| @NotNull |
| public static SlingUriBuilder createFrom(@NotNull SlingHttpServletRequest request) { |
| return createFrom(request.getRequestPathInfo()) |
| .setResourceResolver(request.getResourceResolver()) |
| .setScheme(request.getScheme()) |
| .setHost(request.getServerName()) |
| .setPort(request.getServerPort()) |
| .setQuery(request.getQueryString()); |
| } |
| |
| /** |
| * Creates a builder from an arbitrary URI. |
| * |
| * @param uri the uri to transform to a SlingUri |
| * @param resourceResolver a resource resolver is needed to decide up to what part the path is the resource path (that decision is only |
| * possible by checking against the underlying repository). If null is passed in, the shortest viable resource path is used. |
| * @return a SlingUriBuilder |
| */ |
| @NotNull |
| public static SlingUriBuilder createFrom(@NotNull URI uri, @Nullable ResourceResolver resourceResolver) { |
| String path = uri.getRawPath(); |
| boolean pathExists = isNotBlank(path); |
| String uriQuery = uri.getRawQuery(); |
| boolean schemeSpecificRelevant = !pathExists && uriQuery == null; |
| String uriHost = uri.getHost(); |
| if (FILE_SCHEME.equals(uri.getScheme()) && uriHost == null) { |
| uriHost = ""; // ensure three slashes in file URIs without host |
| } |
| return create() |
| .setResourceResolver(resourceResolver) |
| .setScheme(uri.getScheme()) |
| .setUserInfo(uri.getRawUserInfo()) |
| .setHost(uriHost) |
| .setPort(uri.getPort()) |
| .setPath(pathExists ? path : null) |
| .setQuery(uriQuery) |
| .setFragment(uri.getRawFragment()) |
| .setSchemeSpecificPart(schemeSpecificRelevant ? uri.getRawSchemeSpecificPart() : null); |
| } |
| |
| /** |
| * Creates a builder from an arbitrary URI string. |
| * |
| * @param uriStr to uri string to parse |
| * @param resourceResolver a resource resolver is needed to decide up to what part the path is the resource path (that decision is only |
| * possible by checking against the underlying repository). If null is passed in, the shortest viable resource path is used. |
| * @return a SlingUriBuilder |
| */ |
| @NotNull |
| public static SlingUriBuilder parse(@NotNull String uriStr, @Nullable ResourceResolver resourceResolver) { |
| URI uri; |
| try { |
| uri = new URI(uriStr); |
| return createFrom(uri, resourceResolver); |
| } catch (URISyntaxException e) { |
| LOG.debug("Invalid URI {}: {}", uriStr, e.getMessage(), e); |
| // best effort to match input, see SlingUriInvalidUrisTest |
| return parseBestEffort(uriStr, resourceResolver); |
| } |
| } |
| |
| private static SlingUriBuilder parseBestEffort(String uriStr, ResourceResolver resourceResolver) { |
| Matcher matcher = Pattern.compile(BEST_EFFORT_INVALID_URI_MATCHER).matcher(uriStr); |
| matcher.find(); |
| |
| String scheme = matcher.group(1); |
| String userInfo = matcher.group(2); |
| String host = matcher.group(3); |
| String port = matcher.groupCount() >= 4 ? matcher.group(4) : null; |
| String path = matcher.groupCount() >= 5 ? matcher.group(5) : null; |
| String query = matcher.groupCount() >= 6 ? matcher.group(6) : null; |
| String fragment = matcher.groupCount() >= 7 ? matcher.group(7) : null; |
| if (!isBlank(scheme) && isBlank(host)) { |
| // opaque case |
| return create() |
| .setResourceResolver(resourceResolver) |
| .setScheme(scheme) |
| .setSchemeSpecificPart(path) |
| .setFragment(fragment); |
| } else if (!isBlank(host) || !isBlank(path)) { |
| return create() |
| .setResourceResolver(resourceResolver) |
| .setScheme(scheme) |
| .setUserInfo(userInfo) |
| .setHost(host) |
| .setPort(port != null ? Integer.parseInt(port) : -1) |
| .setPath(path) |
| .setQuery(query) |
| .setFragment(fragment); |
| } else { |
| return create() |
| .setResourceResolver(resourceResolver) |
| .setSchemeSpecificPart(uriStr); |
| } |
| } |
| |
| // simple helper to avoid StringUtils dependency |
| private static boolean isBlank(final CharSequence cs) { |
| return cs == null || cs.chars().allMatch(Character::isWhitespace); |
| } |
| |
| private static boolean isNotBlank(final CharSequence cs) { |
| return !isBlank(cs); |
| } |
| |
| private String scheme = null; |
| |
| private String userInfo = null; |
| private String host = null; |
| private int port = -1; |
| |
| private String resourcePath = null; |
| private final List<String> selectors = new LinkedList<>(); |
| private String extension = null; |
| private final Map<String, String> pathParameters = new LinkedHashMap<>(); |
| private String suffix = null; |
| private String schemeSpecificPart = null; |
| private String query = null; |
| private String fragment = null; |
| |
| // needed for getSuffixResource() from interface RequestPathInfo and rebaseResourcePath() |
| private ResourceResolver resourceResolver = null; |
| |
| // to ensure a builder is used only once (as the ImmutableSlingUri being created in build() is sharing its state) |
| private boolean isBuilt = false; |
| |
| private SlingUriBuilder() { |
| } |
| |
| /** |
| * Set the user info of the URI. |
| * |
| * @param userInfo the user info |
| * @return the builder for method chaining |
| */ |
| @NotNull |
| public SlingUriBuilder setUserInfo(@Nullable String userInfo) { |
| if (schemeSpecificPart != null) { |
| return this; |
| } |
| this.userInfo = userInfo; |
| return this; |
| } |
| |
| /** |
| * Set the host of the URI. |
| * |
| * @param host the host |
| * @return the builder for method chaining |
| */ |
| @NotNull |
| public SlingUriBuilder setHost(@Nullable String host) { |
| if (schemeSpecificPart != null) { |
| return this; |
| } |
| this.host = host; |
| return this; |
| } |
| |
| /** |
| * Set the port of the URI. |
| * |
| * @param port the port |
| * @return the builder for method chaining |
| */ |
| @NotNull |
| public SlingUriBuilder setPort(int port) { |
| if (schemeSpecificPart != null) { |
| return this; |
| } |
| this.port = port; |
| return this; |
| } |
| |
| /** |
| * Set the path of the URI that contains a resource path and optionally path parameters, selectors, an extension and a suffix. To remove |
| * an existing path set path to {@code null}. |
| * |
| * @param path the path |
| * @return the builder for method chaining |
| */ |
| @NotNull |
| public SlingUriBuilder setPath(@Nullable String path) { |
| if (schemeSpecificPart != null) { |
| return this; |
| } |
| |
| // adds path parameters to this.pathParameters and returns path without those |
| path = extractPathParameters(path); |
| |
| // split in resource path, selectors, extension and suffix |
| Matcher dotMatcher; |
| if (path != null && path.startsWith(SlingUriBuilder.CHAR_SLASH) && resourceResolver != null) { |
| setResourcePath(path); |
| rebaseResourcePath(); |
| } else if (path != null && (dotMatcher = Pattern.compile(SELECTOR_DOT_REGEX).matcher(path)).find()) { |
| int firstDotPosition = dotMatcher.start(); |
| setPathWithDefinedResourcePosition(path, firstDotPosition); |
| } else { |
| setSelectors(new String[] {}); |
| setSuffix(null); |
| setExtension(null); |
| setResourcePath(path); |
| } |
| |
| return this; |
| } |
| |
| /** |
| * Will rebase the uri based on the underlying resource structure. A resource resolver is necessary for this operation, hence |
| * setResourceResolver() needs to be called before balanceResourcePath() or a create method that implicitly sets this has to be used. |
| * |
| * @return the builder for method chaining |
| * @throws IllegalStateException |
| * if no resource resolver is available |
| */ |
| @NotNull |
| public SlingUriBuilder rebaseResourcePath() { |
| if (schemeSpecificPart != null || resourcePath == null) { |
| return this; |
| } |
| if (resourceResolver == null) { |
| throw new IllegalStateException("setResourceResolver() needs to be called before balanceResourcePath()"); |
| } |
| |
| String path = assemblePath(false); |
| if (path == null) { |
| return this; // nothing to rebase |
| } |
| ResourcePathIterator it = new ResourcePathIterator(path); |
| String availableResourcePath = null; |
| while (it.hasNext()) { |
| availableResourcePath = it.next(); |
| if (resourceResolver.getResource(availableResourcePath) != null) { |
| break; |
| } |
| } |
| if (availableResourcePath == null) { |
| return this; // nothing to rebase |
| } |
| |
| selectors.clear(); |
| extension = null; |
| suffix = null; |
| if (availableResourcePath.length() == path.length()) { |
| resourcePath = availableResourcePath; |
| } else { |
| setPathWithDefinedResourcePosition(path, availableResourcePath.length()); |
| } |
| return this; |
| } |
| |
| /** |
| * Set the resource path of the URI. |
| * |
| * @param resourcePath the resource path |
| * @return the builder for method chaining |
| */ |
| @NotNull |
| public SlingUriBuilder setResourcePath(@Nullable String resourcePath) { |
| if (schemeSpecificPart != null) { |
| return this; |
| } |
| this.resourcePath = resourcePath; |
| return this; |
| } |
| |
| /** |
| * Set the selectors of the URI. |
| * |
| * @param selectors the selectors |
| * @return the builder for method chaining |
| */ |
| @NotNull |
| public SlingUriBuilder setSelectors(@NotNull String[] selectors) { |
| if (schemeSpecificPart != null || resourcePath == null) { |
| return this; |
| } |
| this.selectors.clear(); |
| Arrays.stream(selectors).forEach(this.selectors::add); |
| return this; |
| } |
| |
| /** |
| * Add a selector to the URI. |
| * |
| * @param selector the selector to add |
| * @return the builder for method chaining |
| */ |
| @NotNull |
| public SlingUriBuilder addSelector(@NotNull String selector) { |
| if (schemeSpecificPart != null || resourcePath == null) { |
| return this; |
| } |
| this.selectors.add(selector); |
| return this; |
| } |
| |
| /** |
| * Set the extension of the URI. |
| * |
| * @param extension the extension |
| * @return the builder for method chaining |
| */ |
| @NotNull |
| public SlingUriBuilder setExtension(@Nullable String extension) { |
| if (schemeSpecificPart != null || resourcePath == null) { |
| return this; |
| } |
| this.extension = extension; |
| return this; |
| } |
| |
| /** |
| * Set a path parameter to the URI. |
| * |
| * @param key the path parameter key |
| * @param value the path parameter value |
| * @return the builder for method chaining |
| */ |
| @NotNull |
| public SlingUriBuilder setPathParameter(@NotNull String key, @NotNull String value) { |
| if (schemeSpecificPart != null || resourcePath == null) { |
| return this; |
| } |
| this.pathParameters.put(key, value); |
| return this; |
| } |
| |
| /** |
| * Replaces all path parameters in the URI. |
| * |
| * @param pathParameters the path parameters |
| * @return the builder for method chaining |
| */ |
| @NotNull |
| public SlingUriBuilder setPathParameters(@NotNull Map<String, String> pathParameters) { |
| this.pathParameters.clear(); |
| this.pathParameters.putAll(pathParameters); |
| return this; |
| } |
| |
| /** |
| * Set the suffix of the URI. |
| * |
| * @param suffix the suffix |
| * @return the builder for method chaining |
| */ |
| @NotNull |
| public SlingUriBuilder setSuffix(@Nullable String suffix) { |
| if (schemeSpecificPart != null || resourcePath == null) { |
| return this; |
| } |
| if (suffix != null && !suffix.startsWith("/")) { |
| throw new IllegalArgumentException("Suffix needs to start with slash"); |
| } |
| this.suffix = suffix; |
| return this; |
| } |
| |
| /** |
| * Set the query of the URI. |
| * |
| * @param query the query |
| * @return the builder for method chaining |
| */ |
| @NotNull |
| public SlingUriBuilder setQuery(@Nullable String query) { |
| if (schemeSpecificPart != null) { |
| return this; |
| } |
| this.query = query; |
| return this; |
| } |
| |
| /** |
| * Add a query parameter to the query of the URI. Key and value are URL-encoded before adding the parameter to the query string. |
| * |
| * @param parameterName the parameter name |
| * @param value the parameter value |
| * @return the builder for method chaining |
| */ |
| @NotNull |
| public SlingUriBuilder addQueryParameter(@NotNull String parameterName, @NotNull String value) { |
| if (schemeSpecificPart != null) { |
| return this; |
| } |
| try { |
| this.query = (this.query == null ? "" : this.query + CHAR_AMP) |
| + URLEncoder.encode(parameterName, StandardCharsets.UTF_8.name()) |
| + "=" + URLEncoder.encode(value, StandardCharsets.UTF_8.name()); |
| } catch (UnsupportedEncodingException e) { |
| throw new IllegalStateException("Encoding not supported: " + StandardCharsets.UTF_8, e); |
| } |
| return this; |
| } |
| |
| /** |
| * <p> |
| * Replace all query parameters of the URL. Both keys and values are URL-encoded before adding them to the query string. |
| * </p> |
| * <p> |
| * For adding multiple query parameters with the same name prefer to use {@link #addQueryParameter(String, String)}. |
| * </p> |
| * |
| * @param queryParameters the map with the query parameters |
| * @return the builder for method chaining |
| */ |
| @NotNull |
| public SlingUriBuilder setQueryParameters(@NotNull Map<String, String> queryParameters) { |
| if (schemeSpecificPart != null) { |
| return this; |
| } |
| setQuery(null); // reset first |
| for (Map.Entry<String, String> parameter : queryParameters.entrySet()) { |
| addQueryParameter(parameter.getKey(), parameter.getValue()); |
| } |
| return this; |
| } |
| |
| /** |
| * Set the fragment of the URI. |
| * |
| * @param fragment the fragment |
| * @return the builder for method chaining |
| */ |
| @NotNull |
| public SlingUriBuilder setFragment(@Nullable String fragment) { |
| this.fragment = fragment; |
| return this; |
| } |
| |
| /** |
| * Set the scheme of the URI. |
| * |
| * @param scheme the scheme |
| * @return the builder for method chaining |
| */ |
| @NotNull |
| public SlingUriBuilder setScheme(@Nullable String scheme) { |
| this.scheme = scheme; |
| return this; |
| } |
| |
| /** |
| * Set the scheme specific part of the URI. Use this for e.g. mail:jon@example.com URIs. |
| * |
| * @param schemeSpecificPart the scheme specific part |
| * @return the builder for method chaining |
| */ |
| @NotNull |
| public SlingUriBuilder setSchemeSpecificPart(@Nullable String schemeSpecificPart) { |
| this.schemeSpecificPart = schemeSpecificPart; |
| return this; |
| } |
| |
| /** |
| * Will remove scheme and authority (that is user info, host and port). |
| * |
| * @return the builder for method chaining |
| */ |
| @NotNull |
| public SlingUriBuilder removeSchemeAndAuthority() { |
| setScheme(null); |
| setUserInfo(null); |
| setHost(null); |
| setPort(-1); |
| return this; |
| } |
| |
| /** |
| * Will take over scheme and authority (user info, host and port) from provided slingUri. |
| * |
| * @param slingUri the Sling URI |
| * @return the builder for method chaining |
| */ |
| @NotNull |
| public SlingUriBuilder useSchemeAndAuthority(@NotNull SlingUri slingUri) { |
| setScheme(slingUri.getScheme()); |
| setUserInfo(slingUri.getUserInfo()); |
| setHost(slingUri.getHost()); |
| setPort(slingUri.getPort()); |
| return this; |
| } |
| |
| /** |
| * Returns the resource path. |
| * |
| * @return returns the resource path or null if the URI does not contain a path. |
| */ |
| @Nullable |
| public String getResourcePath() { |
| return resourcePath; |
| } |
| |
| /** |
| * Returns the selector string |
| * |
| * @return returns the selector string or null if the URI does not contain selector(s) (in line with {@link RequestPathInfo}) |
| */ |
| @Nullable |
| public String getSelectorString() { |
| return !selectors.isEmpty() ? String.join(CHAR_DOT, selectors) : null; |
| } |
| |
| /** |
| * Returns the selectors array. |
| * |
| * @return the selectors array (empty if the URI does not contain selector(s), never null) |
| */ |
| @NotNull |
| public String[] getSelectors() { |
| return selectors.toArray(new String[selectors.size()]); |
| } |
| |
| /** |
| * Returns the extension. |
| * |
| * @return the extension or null if the URI does not contain an extension |
| */ |
| @Nullable |
| public String getExtension() { |
| return extension; |
| } |
| |
| /** |
| * Returns the path parameters. |
| * |
| * @return the path parameters or an empty Map if the URI does not contain any |
| */ |
| @Nullable |
| public Map<String, String> getPathParameters() { |
| return pathParameters; |
| } |
| |
| /** |
| * Returns the suffix part of the URI |
| * |
| * @return the suffix string or null if the URI does not contain a suffix |
| */ |
| @Nullable |
| public String getSuffix() { |
| return suffix; |
| } |
| |
| /** |
| * Returns the corresponding suffix resource or null if |
| * <ul> |
| * <li>no resource resolver is available (depends on the create method used in SlingUriBuilder)</li> |
| * <li>the URI does not contain a suffix</li> |
| * <li>if the suffix resource could not be found</li> |
| * </ul> |
| * |
| * @return the suffix resource if available or null |
| */ |
| @Nullable |
| public Resource getSuffixResource() { |
| if (isNotBlank(suffix) && resourceResolver != null) { |
| return resourceResolver.getResource(suffix); |
| } else { |
| return null; |
| } |
| } |
| |
| /** |
| * Returns the joint path of resource path, selectors, extension and suffix. |
| * |
| * @return the path or null if no path is set |
| */ |
| @Nullable |
| public String getPath() { |
| return assemblePath(true); |
| } |
| |
| /** |
| * Returns the scheme-specific part of the URI, compare with Javadoc of {@link URI}. |
| * |
| * @return scheme specific part of the URI |
| */ |
| @Nullable |
| public String getSchemeSpecificPart() { |
| if (isOpaque()) { |
| return schemeSpecificPart; |
| } else { |
| return toStringInternal(false, false); |
| } |
| } |
| |
| /** |
| * Returns the query. |
| * |
| * @return the query part of the URI or null if the URI does not contain a query |
| */ |
| @Nullable |
| public String getQuery() { |
| return query; |
| } |
| |
| /** |
| * Returns the fragment. |
| * |
| * @return the fragment or null if the URI does not contain a fragment |
| */ |
| @Nullable |
| public String getFragment() { |
| return fragment; |
| } |
| |
| /** |
| * Returns the scheme. |
| * |
| * @return the scheme or null if not set |
| */ |
| @Nullable |
| public String getScheme() { |
| return scheme; |
| } |
| |
| /** |
| * Returns the host. |
| * |
| * @return returns the host of the SlingUri or null if not set |
| */ |
| @Nullable |
| public String getHost() { |
| return host; |
| } |
| |
| /** |
| * Returns the port. |
| * |
| * @return returns the port of the SlingUri or -1 if not set |
| */ |
| public int getPort() { |
| return port; |
| } |
| |
| /** |
| * Returns the user info. |
| * |
| * @return the user info of the SlingUri or null if not set |
| */ |
| @Nullable |
| public String getUserInfo() { |
| return userInfo; |
| } |
| |
| /** |
| * Will take over scheme and authority (user info, host and port) from provided URI. |
| * |
| * @param uri the URI |
| * @return the builder for method chaining |
| */ |
| @NotNull |
| public SlingUriBuilder useSchemeAndAuthority(@NotNull URI uri) { |
| useSchemeAndAuthority(createFrom(uri, resourceResolver).build()); |
| return this; |
| } |
| |
| /** |
| * Sets the resource resolver (required for {@link RequestPathInfo#getSuffixResource()}). |
| * |
| * @param resourceResolver the resource resolver |
| * @return the builder for method chaining |
| */ |
| @NotNull |
| public SlingUriBuilder setResourceResolver(ResourceResolver resourceResolver) { |
| this.resourceResolver = resourceResolver; |
| return this; |
| } |
| |
| /** Builds the immutable SlingUri from this builder. |
| * |
| * @return the builder for method chaining */ |
| @NotNull |
| public SlingUri build() { |
| if (isBuilt) { |
| throw new IllegalStateException("SlingUriBuilder.build() may only be called once per builder instance"); |
| } |
| isBuilt = true; |
| return new ImmutableSlingUri(); |
| } |
| |
| /** |
| * Builds the corresponding string URI for this builder. |
| * |
| * @return string representation of builder |
| */ |
| public String toString() { |
| return toStringInternal(true, true); |
| } |
| |
| /** |
| * Returns true the URI is either a relative or absolute path (this is the case if scheme and host is empty and the URI path is set) |
| * |
| * @return returns true for path URIs |
| */ |
| public boolean isPath() { |
| return isBlank(scheme) |
| && isBlank(host) |
| && isNotBlank(resourcePath); |
| } |
| |
| /** |
| * Returns true if the URI has an absolute path starting with a slash ('/'). |
| * |
| * @return true if the URI is an absolute path |
| */ |
| public boolean isAbsolutePath() { |
| return isPath() && resourcePath.startsWith(SlingUriBuilder.CHAR_SLASH); |
| } |
| |
| /** |
| * Returns true if the URI is a relative path (no scheme and path does not start with '/'). |
| * |
| * @return true if URI is a relative path |
| */ |
| public boolean isRelativePath() { |
| return isPath() && !resourcePath.startsWith(SlingUriBuilder.CHAR_SLASH); |
| } |
| |
| /** |
| * Returns true the URI is an absolute URI. |
| * |
| * @return true if the URI is an absolute URI containing a scheme. |
| */ |
| public boolean isAbsolute() { |
| return scheme != null; |
| } |
| |
| /** |
| * Returns true for opaque URIs like e.g. mailto:jon@example.com. |
| * |
| * @return true if the URI is an opaque URI |
| */ |
| public boolean isOpaque() { |
| return scheme != null && schemeSpecificPart != null; |
| } |
| |
| private String toStringInternal(boolean includeScheme, boolean includeFragment) { |
| StringBuilder requestUri = new StringBuilder(); |
| |
| if (includeScheme && isAbsolute()) { |
| requestUri.append(scheme + CHAR_COLON); |
| } |
| if (host != null) { |
| requestUri.append(CHAR_SLASH + CHAR_SLASH); |
| if (isNotBlank(userInfo)) { |
| requestUri.append(userInfo + CHAR_AT); |
| } |
| requestUri.append(host); |
| if (port > 0 |
| && !(HTTP_SCHEME.equals(scheme) && port == HTTP_DEFAULT_PORT) |
| && !(HTTPS_SCHEME.equals(scheme) && port == HTTPS_DEFAULT_PORT)) { |
| requestUri.append(CHAR_COLON); |
| requestUri.append(port); |
| } |
| } |
| if (schemeSpecificPart != null) { |
| requestUri.append(schemeSpecificPart); |
| } |
| if (resourcePath != null) { |
| requestUri.append(assemblePath(true)); |
| } |
| if (query != null) { |
| requestUri.append(CHAR_QM + query); |
| } |
| if (includeFragment && fragment != null) { |
| requestUri.append(CHAR_HASH + fragment); |
| } |
| return requestUri.toString(); |
| } |
| |
| private void setPathWithDefinedResourcePosition(String path, int firstDotPositionAfterResourcePath) { |
| setResourcePath(path.substring(0, firstDotPositionAfterResourcePath)); |
| int firstSlashAfterFirstDotPosition = path.indexOf(CHAR_SLASH, firstDotPositionAfterResourcePath); |
| String pathWithoutSuffix = firstSlashAfterFirstDotPosition > -1 |
| ? path.substring(firstDotPositionAfterResourcePath + 1, firstSlashAfterFirstDotPosition) |
| : path.substring(firstDotPositionAfterResourcePath + 1); |
| String[] pathBits = pathWithoutSuffix.split(SELECTOR_DOT_REGEX); |
| if (pathBits.length > 1) { |
| setSelectors(Arrays.copyOfRange(pathBits, 0, pathBits.length - 1)); |
| } |
| setExtension(pathBits.length > 0 && pathBits[pathBits.length - 1].length() > 0 ? pathBits[pathBits.length - 1] : null); |
| setSuffix(firstSlashAfterFirstDotPosition > -1 ? path.substring(firstSlashAfterFirstDotPosition) : null); |
| } |
| |
| private String extractPathParameters(String path) { |
| // we rebuild the parameters from scratch as given in path (if path is set to null we also reset) |
| pathParameters.clear(); |
| if (path != null) { |
| Pattern pathParameterRegex = Pattern.compile(PATH_PARAMETERS_REGEX); |
| |
| StringBuffer resultString = null; |
| Matcher regexMatcher = pathParameterRegex.matcher(path); |
| while (regexMatcher.find()) { |
| if (resultString == null) { |
| resultString = new StringBuffer(); |
| } |
| regexMatcher.appendReplacement(resultString, ""); |
| String key = regexMatcher.group(1); |
| String value = isNotBlank(regexMatcher.group(2)) ? regexMatcher.group(2) : regexMatcher.group(3); |
| pathParameters.put(key, value); |
| } |
| if (resultString != null) { |
| regexMatcher.appendTail(resultString); |
| path = resultString.toString(); |
| } |
| } |
| return path; |
| } |
| |
| private String assemblePath(boolean includePathParamters) { |
| if (resourcePath == null) { |
| return null; |
| } |
| |
| StringBuilder pathBuilder = new StringBuilder(); |
| pathBuilder.append(resourcePath); |
| if (includePathParamters && !pathParameters.isEmpty()) { |
| for (Map.Entry<String, String> pathParameter : pathParameters.entrySet()) { |
| pathBuilder.append(CHAR_SEMICOLON + pathParameter.getKey() + CHAR_EQUALS + |
| CHAR_SINGLEQUOTE + pathParameter.getValue() + CHAR_SINGLEQUOTE); |
| } |
| } |
| |
| boolean dotAdded = false; |
| if (!selectors.isEmpty()) { |
| pathBuilder.append(CHAR_DOT + String.join(CHAR_DOT, selectors)); |
| dotAdded = true; |
| } |
| if (isNotBlank(extension)) { |
| pathBuilder.append(CHAR_DOT + extension); |
| dotAdded = true; |
| } |
| |
| if (isNotBlank(suffix)) { |
| if (!dotAdded) { |
| pathBuilder.append(CHAR_DOT); |
| } |
| pathBuilder.append(suffix); |
| } |
| return pathBuilder.toString(); |
| } |
| |
| |
| // read-only view on the builder data (to avoid another copy of the data into a new object) |
| private class ImmutableSlingUri implements SlingUri { |
| |
| @Override |
| public String getResourcePath() { |
| return getData().getResourcePath(); |
| } |
| |
| @Override |
| public String getSelectorString() { |
| return getData().getSelectorString(); |
| } |
| |
| @Override |
| public String[] getSelectors() { |
| return getData().getSelectors(); |
| } |
| |
| @Override |
| public String getExtension() { |
| return getData().getExtension(); |
| } |
| |
| @Override |
| public Map<String, String> getPathParameters() { |
| return Collections.unmodifiableMap(getData().getPathParameters()); |
| } |
| |
| @Override |
| public String getSuffix() { |
| return getData().getSuffix(); |
| } |
| |
| @Override |
| public String getPath() { |
| return getData().getPath(); |
| } |
| |
| @Override |
| public String getSchemeSpecificPart() { |
| return getData().getSchemeSpecificPart(); |
| } |
| |
| @Override |
| public String getQuery() { |
| return getData().getQuery(); |
| } |
| |
| @Override |
| public String getFragment() { |
| return getData().getFragment(); |
| } |
| |
| @Override |
| public String getScheme() { |
| return getData().getScheme(); |
| } |
| |
| @Override |
| public String getHost() { |
| return getData().getHost(); |
| } |
| |
| @Override |
| public int getPort() { |
| return getData().getPort(); |
| } |
| |
| @Override |
| public Resource getSuffixResource() { |
| return getData().getSuffixResource(); |
| } |
| |
| @Override |
| public String getUserInfo() { |
| return getData().getUserInfo(); |
| } |
| |
| @Override |
| public boolean isOpaque() { |
| return getData().isOpaque(); |
| } |
| |
| @Override |
| public boolean isPath() { |
| return getData().isPath(); |
| } |
| |
| @Override |
| public boolean isAbsolutePath() { |
| return getData().isAbsolutePath(); |
| } |
| |
| @Override |
| public boolean isRelativePath() { |
| return getData().isRelativePath(); |
| } |
| |
| @Override |
| public boolean isAbsolute() { |
| return getData().isAbsolute(); |
| } |
| |
| @Override |
| public String toString() { |
| return getData().toString(); |
| } |
| |
| @Override |
| public URI toUri() { |
| String uriString = toString(); |
| try { |
| return new URI(uriString); |
| } catch (URISyntaxException e) { |
| throw new IllegalStateException("Invalid Sling URI: " + uriString, e); |
| } |
| } |
| |
| private SlingUriBuilder getData() { |
| return SlingUriBuilder.this; |
| } |
| |
| // generated hashCode() and equals() |
| @Override |
| public int hashCode() { |
| final int prime = 31; |
| int result = 1; |
| result = prime * result + ((extension == null) ? 0 : extension.hashCode()); |
| result = prime * result + ((fragment == null) ? 0 : fragment.hashCode()); |
| result = prime * result + ((host == null) ? 0 : host.hashCode()); |
| result = prime * result + pathParameters.hashCode(); |
| result = prime * result + port; |
| result = prime * result + ((query == null) ? 0 : query.hashCode()); |
| result = prime * result + ((resourcePath == null) ? 0 : resourcePath.hashCode()); |
| result = prime * result + ((scheme == null) ? 0 : scheme.hashCode()); |
| result = prime * result + ((schemeSpecificPart == null) ? 0 : schemeSpecificPart.hashCode()); |
| result = prime * result + selectors.hashCode(); |
| result = prime * result + ((suffix == null) ? 0 : suffix.hashCode()); |
| result = prime * result + ((userInfo == null) ? 0 : userInfo.hashCode()); |
| return result; |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (this == obj) |
| return true; |
| if (obj == null) |
| return false; |
| if (getClass() != obj.getClass()) |
| return false; |
| ImmutableSlingUri other = (ImmutableSlingUri) obj; |
| if (extension == null) { |
| if (other.getData().extension != null) |
| return false; |
| } else if (!extension.equals(other.getData().extension)) |
| return false; |
| if (fragment == null) { |
| if (other.getData().fragment != null) |
| return false; |
| } else if (!fragment.equals(other.getData().fragment)) |
| return false; |
| if (host == null) { |
| if (other.getData().host != null) |
| return false; |
| } else if (!host.equals(other.getData().host)) |
| return false; |
| if (pathParameters == null) { |
| if (other.getData().pathParameters != null) |
| return false; |
| } else if (!pathParameters.equals(other.getData().pathParameters)) |
| return false; |
| if (port != other.getData().port) |
| return false; |
| if (query == null) { |
| if (other.getData().query != null) |
| return false; |
| } else if (!query.equals(other.getData().query)) |
| return false; |
| if (resourcePath == null) { |
| if (other.getData().resourcePath != null) |
| return false; |
| } else if (!resourcePath.equals(other.getData().resourcePath)) |
| return false; |
| if (scheme == null) { |
| if (other.getData().scheme != null) |
| return false; |
| } else if (!scheme.equals(other.getData().scheme)) |
| return false; |
| if (schemeSpecificPart == null) { |
| if (other.getData().schemeSpecificPart != null) |
| return false; |
| } else if (!schemeSpecificPart.equals(other.getData().schemeSpecificPart)) |
| return false; |
| if (selectors == null) { |
| if (other.getData().selectors != null) |
| return false; |
| } else if (!selectors.equals(other.getData().selectors)) |
| return false; |
| if (suffix == null) { |
| if (other.getData().suffix != null) |
| return false; |
| } else if (!suffix.equals(other.getData().suffix)) |
| return false; |
| if (userInfo == null) { |
| if (other.getData().userInfo != null) |
| return false; |
| } else if (!userInfo.equals(other.getData().userInfo)) |
| return false; |
| return true; |
| } |
| } |
| |
| /** Iterate over a path by creating shorter segments of that path using "." as a separator. |
| * <p> |
| * For example, if path = /some/path.a4.html/xyz.ext the sequence is: |
| * <ol> |
| * <li>/some/path.a4.html/xyz.ext</li> |
| * <li>/some/path.a4.html/xyz</li> |
| * <li>/some/path.a4</li> |
| * <li>/some/path</li> |
| * </ol> |
| * <p> |
| * The root path (/) is never returned. */ |
| private class ResourcePathIterator implements Iterator<String> { |
| |
| // the next path to return, null if nothing more to return |
| private String nextPath; |
| |
| /** Creates a new instance iterating over the given path |
| * |
| * @param path The path to iterate over. If this is empty or <code>null</code> this iterator will not return anything. */ |
| private ResourcePathIterator(String path) { |
| if (path == null || path.length() == 0) { |
| // null or empty path, there is nothing to return |
| nextPath = null; |
| } else { |
| // find last non-slash character |
| int i = path.length() - 1; |
| while (i >= 0 && path.charAt(i) == '/') { |
| i--; |
| } |
| if (i < 0) { |
| // only slashes, assume root node |
| nextPath = "/"; |
| } else if (i < path.length() - 1) { |
| // cut off slash |
| nextPath = path.substring(0, i + 1); |
| } else { |
| // no trailing slash |
| nextPath = path; |
| } |
| } |
| } |
| |
| public boolean hasNext() { |
| return nextPath != null; |
| } |
| |
| public String next() { |
| if (!hasNext()) { |
| throw new NoSuchElementException(); |
| } |
| final String result = nextPath; |
| // find next path |
| int lastDot = nextPath.lastIndexOf('.'); |
| nextPath = (lastDot > 0) ? nextPath.substring(0, lastDot) : null; |
| |
| return result; |
| } |
| |
| @Override |
| public void remove() { |
| throw new UnsupportedOperationException("remove"); |
| } |
| } |
| |
| } |