You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by gh...@apache.org on 2020/09/18 08:41:35 UTC

[sling-org-apache-sling-api] branch feature/SLING-9745-ResourceUri-Api created (now 298ed53)

This is an automated email from the ASF dual-hosted git repository.

ghenzler pushed a change to branch feature/SLING-9745-ResourceUri-Api
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-api.git.


      at 298ed53  SLING-9745 Introduce ResourceUri (immutable) and ResourceUriBuilder

This branch includes the following new commits:

     new 298ed53  SLING-9745 Introduce ResourceUri (immutable) and ResourceUriBuilder

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[sling-org-apache-sling-api] 01/01: SLING-9745 Introduce ResourceUri (immutable) and ResourceUriBuilder

Posted by gh...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ghenzler pushed a commit to branch feature/SLING-9745-ResourceUri-Api
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-api.git

commit 298ed530c6bbd6bcc828c1c9c80790d9919c7f43
Author: georg.henzler <ge...@netcentric.biz>
AuthorDate: Fri Sep 18 10:41:15 2020 +0200

    SLING-9745 Introduce ResourceUri (immutable) and ResourceUriBuilder
    
    General purpose class to represent a ResourceUri (like e.g. a link in an
    html <a> tag)
---
 pom.xml                                            |   1 -
 .../apache/sling/api/resource/uri/ResourceUri.java | 181 +++++
 .../sling/api/resource/uri/ResourceUriBuilder.java | 837 +++++++++++++++++++++
 .../sling/api/resource/uri/package-info.java       |  23 +
 .../api/resource/uri/ResourceUriBuilderTest.java   | 157 ++++
 .../ResourceUriBuilderWithAdjustMethodTest.java    | 244 ++++++
 .../api/resource/uri/ResourceUriRebaseTest.java    | 231 ++++++
 .../sling/api/resource/uri/ResourceUriTest.java    | 442 +++++++++++
 ...UriToSlingRequestPathInfoCompatibilityTest.java | 272 +++++++
 9 files changed, 2387 insertions(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 8e02cb7..38e6e92 100644
--- a/pom.xml
+++ b/pom.xml
@@ -105,7 +105,6 @@
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-lang3</artifactId>
             <version>3.2</version>
-            <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>org.osgi</groupId>
diff --git a/src/main/java/org/apache/sling/api/resource/uri/ResourceUri.java b/src/main/java/org/apache/sling/api/resource/uri/ResourceUri.java
new file mode 100644
index 0000000..1250215
--- /dev/null
+++ b/src/main/java/org/apache/sling/api/resource/uri/ResourceUri.java
@@ -0,0 +1,181 @@
+/*
+ * 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.resource.uri;
+
+import static org.apache.commons.lang3.StringUtils.isBlank;
+import static org.apache.commons.lang3.StringUtils.isNotBlank;
+
+import java.net.URI;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import org.apache.sling.api.request.RequestPathInfo;
+import org.apache.sling.api.resource.Resource;
+
+/**
+ * Represents an immutable URI that points to a resource or alternatively, can contain opaque URIs like {@code mailto:} or
+ * {@code javascript:}. Use {@link ResourceUri#adjust(Consumer)} or {@link ResourceUriBuilder} to create new or modified instances.
+ */
+public interface ResourceUri extends RequestPathInfo {
+
+    /**
+     * @return returns the URI.
+     */
+    public URI toUri();
+
+    /**
+     * @return returns the URI as String.
+     */
+    public String toString();
+
+    /**
+     * @return returns the scheme of the ResourceUri or null if not set
+     */
+    public String getScheme();
+
+    /**
+     * @return returns the user info of the ResourceUri or null if not set
+     */
+    public String getUserInfo();
+
+    /**
+     * @return returns the host of the ResourceUri or null if not set
+     */
+    public String getHost();
+
+    /**
+     * @return returns the port of the ResourceUri or null if not set
+     */
+    public int getPort();
+
+    /**
+     * @return returns the resource path or null if the URI does not contain a path.
+     */
+    @Override
+    public String getResourcePath();
+
+    /**
+     * @return returns the selector string or null if the URI does not contain selector(s)
+     */
+    @Override
+    public String getSelectorString();
+
+    /**
+     * @return returns the selector array (empty if the URI does not contain selector(s))
+     */
+    @Override
+    public String[] getSelectors();
+
+    /**
+     * @return returns the extension or null if the URI does not contain an extension
+     */
+    @Override
+    public String getExtension();
+
+    /**
+     * @return returns the path parameters or an empty Map if the URI does not contain any
+     */
+    public Map<String, String> getPathParameters();
+
+    /**
+     * @return returns the suffix or null if the URI does not contain a suffix
+     */
+    @Override
+    public String getSuffix();
+
+    /**
+     * @return returns the joint path of resource path, selectors, extension and suffix or null if resource path is not set
+     */
+    public String getPath();
+
+    /**
+     * @return returns the query part of the uri
+     */
+    public String getQuery();
+
+    /**
+     * @return returns the url fragment of the uri
+     */
+    public String getFragment();
+
+    /**
+     * @return scheme specific part of the URI
+     */
+    public String getSchemeSpecificPart();
+
+    /**
+     * @return returns the corresponding suffix resource
+     */
+    @Override
+    public Resource getSuffixResource();
+
+    /**
+     * @return returns true if 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)
+     */
+    default boolean isPath() {
+        return isBlank(getScheme())
+                && isBlank(getHost())
+                && isNotBlank(getResourcePath());
+    }
+
+    /**
+     * @return true if the uri is a absolute path starting with a slash ('/'). This is the default case for all links to pages and assets in
+     *         AEM.
+     */
+    default boolean isAbsolutePath() {
+        return isPath() && getResourcePath().startsWith(ResourceUriBuilder.CHAR_SLASH);
+    }
+
+    /**
+     * @return true if uri is relative (not an URL and not starting with '/')
+     */
+    default boolean isRelativePath() {
+        return isPath() && !getResourcePath().startsWith(ResourceUriBuilder.CHAR_SLASH);
+    }
+
+    /**
+     * @return true if the uri is an absolute URI containing a scheme.
+     */
+    default boolean isFullUri() {
+        return isNotBlank(getScheme())
+                && isNotBlank(getHost());
+    }
+
+    /**
+     * @return true if the uri is an opaque URI like e.g. mailto:
+     */
+    default boolean isOpaque() {
+        return isNotBlank(getScheme())
+                && isNotBlank(getSchemeSpecificPart());
+    }
+
+    /**
+     * Shortcut to adjust resource URIs, e.g. {@code resourceUri = resourceUri.adjust(b -> b.setExtension("html")); }.
+     * 
+     * @param builderConsumer
+     * @return the adjusted ResourceUri (new instance)
+     */
+    default ResourceUri adjust(Consumer<ResourceUriBuilder> builderConsumer) {
+        ResourceUriBuilder builder = ResourceUriBuilder.createFrom(this);
+        builderConsumer.accept(builder);
+        return builder.build();
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/api/resource/uri/ResourceUriBuilder.java b/src/main/java/org/apache/sling/api/resource/uri/ResourceUriBuilder.java
new file mode 100644
index 0000000..09e5d93
--- /dev/null
+++ b/src/main/java/org/apache/sling/api/resource/uri/ResourceUriBuilder.java
@@ -0,0 +1,837 @@
+/*
+ * 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.resource.uri;
+
+import static org.apache.commons.lang3.StringUtils.isNotBlank;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+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.commons.lang3.StringUtils;
+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;
+
+/**
+ * Builder for ResourceUris.
+ */
+public class ResourceUriBuilder {
+
+    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;
+
+    static final String CHAR_HASH = "#";
+    static final String CHAR_QM = "?";
+    static final String CHAR_DOT = ".";
+    static final String CHAR_SLASH = "/";
+    static final String CHAR_AT = "@";
+    static final String SELECTOR_DOT_REGEX = "\\.(?!\\.?/)"; // (?!\\.?/) to avoid matching ./ and ../
+    static final String CHAR_COLON = ":";
+    static final String CHAR_SEMICOLON = ";";
+    static final String CHAR_EQUALS = "=";
+    static final String CHAR_SINGLEQUOTE = "'";
+    static final String PATH_PARAMETERS_REGEX = ";([a-zA-z0-9]+)=(?:\\'([^']*)\\'|([^/]+))";
+
+    public static ResourceUriBuilder create() {
+        return new ResourceUriBuilder();
+    }
+
+    /**
+     * Creates a builder from another ResourceUri.
+     * 
+     * @param resourceUri
+     * @return a ResourceUriBuilder
+     */
+    public static ResourceUriBuilder createFrom(ResourceUri resourceUri) {
+        return create()
+                .setScheme(resourceUri.getScheme())
+                .setUserInfo(resourceUri.getUserInfo())
+                .setHost(resourceUri.getHost())
+                .setPort(resourceUri.getPort())
+                .setResourcePath(resourceUri.getResourcePath())
+                .setPathParameters(resourceUri.getPathParameters())
+                .setSelectors(resourceUri.getSelectors())
+                .setExtension(resourceUri.getExtension())
+                .setSuffix(resourceUri.getSuffix())
+                .setQuery(resourceUri.getQuery())
+                .setFragment(resourceUri.getFragment())
+                .setSchemeSpecificPart(resourceUri.getSchemeSpecificPart())
+                .setResourceResolver(resourceUri instanceof ImmutableResourceUri
+                        ? ((ImmutableResourceUri) resourceUri).getBuilder().resourceResolver
+                        : null);
+    }
+
+    /**
+     * Creates a builder from a Resource (only taking the resource path into account).
+     * 
+     * @param resource
+     * @return a ResourceUriBuilder
+     */
+    public static ResourceUriBuilder createFrom(Resource resource) {
+        return create()
+                .setResourcePath(resource.getPath())
+                .setResourceResolver(resource.getResourceResolver());
+    }
+
+    /**
+     * Creates a builder from a RequestPathInfo instance .
+     * 
+     * @param requestPathInfo
+     * @return a ResourceUriBuilder
+     */
+    public static ResourceUriBuilder createFrom(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
+     * @return a ResourceUriBuilder
+     */
+    public static ResourceUriBuilder createFrom(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 ResourceUri
+     * @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 ResourceUriBuilder
+     */
+    public static ResourceUriBuilder createFrom(@NotNull URI uri, @Nullable ResourceResolver resourceResolver) {
+        String path = uri.getPath();
+        boolean pathExists = !StringUtils.isBlank(path);
+        boolean schemeSpecificRelevant = !pathExists && uri.getQuery() == null;
+        return create()
+                .setResourceResolver(resourceResolver)
+                .setScheme(uri.getScheme())
+                .setUserInfo(uri.getUserInfo())
+                .setHost(uri.getHost())
+                .setPort(uri.getPort())
+                .setPath(pathExists ? path : null)
+                .setQuery(uri.getQuery())
+                .setFragment(uri.getFragment())
+                .setSchemeSpecificPart(schemeSpecificRelevant ? uri.getSchemeSpecificPart() : 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 ResourceUriBuilder
+     */
+    public static ResourceUriBuilder parse(@NotNull String uriStr, @Nullable ResourceResolver resourceResolver) {
+        URI uri;
+        try {
+            uri = new URI(uriStr);
+            return createFrom(uri, resourceResolver);
+        } catch (URISyntaxException e) {
+            throw new IllegalArgumentException("Invalid URI " + uriStr + ": " + e.getMessage(), e);
+        }
+    }
+
+    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;
+
+    // only needed for getSuffixResource() from interface RequestPathInfo
+    private ResourceResolver resourceResolver = null;
+
+    private ResourceUriBuilder() {
+    }
+
+    /** @param userInfo
+     * @return the builder for method chaining */
+    public ResourceUriBuilder setUserInfo(String userInfo) {
+        if (schemeSpecificPart != null) {
+            return this;
+        }
+        this.userInfo = userInfo;
+        return this;
+    }
+
+    /** @param host
+     * @return the builder for method chaining */
+    public ResourceUriBuilder setHost(String host) {
+        if (schemeSpecificPart != null) {
+            return this;
+        }
+        this.host = host;
+        return this;
+    }
+
+    /** @param port
+     * @return the builder for method chaining */
+    public ResourceUriBuilder setPort(int port) {
+        if (schemeSpecificPart != null) {
+            return this;
+        }
+        this.port = port;
+        return this;
+    }
+
+    /** @param path
+     * @return the builder for method chaining */
+    public ResourceUriBuilder setPath(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(ResourceUriBuilder.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 {
+            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
+     */
+    public ResourceUriBuilder 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);
+        ResourcePathIterator it = new ResourcePathIterator(path);
+        String availableResourcePath = null;
+        while (it.hasNext()) {
+            availableResourcePath = it.next();
+            if (resourceResolver.getResource(availableResourcePath) != null) {
+                break;
+            }
+        }
+
+        selectors.clear();
+        extension = null;
+        suffix = null;
+        if (availableResourcePath.length() == path.length()) {
+            resourcePath = availableResourcePath;
+        } else {
+            setPathWithDefinedResourcePosition(path, availableResourcePath.length());
+        }
+        return this;
+    }
+
+    /**
+     * @param resourcePath
+     * @return the builder for method chaining
+     */
+    public ResourceUriBuilder setResourcePath(String resourcePath) {
+        if (schemeSpecificPart != null) {
+            return this;
+        }
+        this.resourcePath = resourcePath;
+        return this;
+    }
+
+    /**
+     * @param selectors
+     * @return the builder for method chaining
+     */
+    public ResourceUriBuilder setSelectors(String[] selectors) {
+        if (schemeSpecificPart != null || resourcePath == null) {
+            return this;
+        }
+        this.selectors.clear();
+        Arrays.stream(selectors).forEach(this.selectors::add);
+        return this;
+    }
+
+    /**
+     * @param selector
+     * @return the builder for method chaining
+     */
+    public ResourceUriBuilder addSelector(String selector) {
+        if (schemeSpecificPart != null || resourcePath == null) {
+            return this;
+        }
+        this.selectors.add(selector);
+        return this;
+    }
+
+    /**
+     * @param extension
+     * @return the builder for method chaining
+     */
+    public ResourceUriBuilder setExtension(String extension) {
+        if (schemeSpecificPart != null || resourcePath == null) {
+            return this;
+        }
+        this.extension = extension;
+        return this;
+    }
+
+    /**
+     * @return returns the path parameters
+     */
+    public ResourceUriBuilder setPathParameter(String key, String value) {
+        if (schemeSpecificPart != null || resourcePath == null) {
+            return this;
+        }
+        this.pathParameters.put(key, value);
+        return this;
+    }
+
+    public ResourceUriBuilder setPathParameters(Map<String, String> pathParameters) {
+        this.pathParameters.clear();
+        this.pathParameters.putAll(pathParameters);
+        return this;
+    }
+
+    /**
+     * @param suffix
+     * @return the builder for method chaining
+     */
+    public ResourceUriBuilder setSuffix(String suffix) {
+        if (schemeSpecificPart != null || resourcePath == null) {
+            return this;
+        }
+        if (suffix != null && !StringUtils.startsWith(suffix, "/")) {
+            throw new IllegalArgumentException("Suffix needs to start with slash");
+        }
+        this.suffix = suffix;
+        return this;
+    }
+
+    /** @param query
+     * @return the builder for method chaining */
+    public ResourceUriBuilder setQuery(String query) {
+        if (schemeSpecificPart != null) {
+            return this;
+        }
+        this.query = query;
+        return this;
+    }
+
+    /** @param urlFragment
+     * @return the builder for method chaining */
+    public ResourceUriBuilder setFragment(String urlFragment) {
+        if (schemeSpecificPart != null) {
+            return this;
+        }
+        this.fragment = urlFragment;
+        return this;
+    }
+
+    /** @param scheme
+     * @return the builder for method chaining */
+    public ResourceUriBuilder setScheme(String scheme) {
+        this.scheme = scheme;
+        return this;
+    }
+
+    /** @param schemeSpecificPart
+     * @return the builder for method chaining */
+    public ResourceUriBuilder setSchemeSpecificPart(String schemeSpecificPart) {
+        if (schemeSpecificPart != null && schemeSpecificPart.isEmpty()) {
+            return this;
+        }
+        this.schemeSpecificPart = schemeSpecificPart;
+        return this;
+    }
+
+    /** Will remove scheme and authority (that is user info, host and port).
+     * 
+     * @return the builder for method chaining */
+    public ResourceUriBuilder removeSchemeAndAuthority() {
+        setScheme(null);
+        setUserInfo(null);
+        setHost(null);
+        setPort(-1);
+        return this;
+    }
+
+    /** Will take over scheme and authority (user info, host and port) from provided resourceUri.
+     * 
+     * @param resourceUri
+     * @return the builder for method chaining */
+    public ResourceUriBuilder useSchemeAndAuthority(ResourceUri resourceUri) {
+        setScheme(resourceUri.getScheme());
+        setUserInfo(resourceUri.getUserInfo());
+        setHost(resourceUri.getHost());
+        setPort(resourceUri.getPort());
+        return this;
+    }
+
+    /**
+     * Sets the resource resolver (required for {@link RequestPathInfo#getSuffixResource()}).
+     * 
+     * @param resourceResolver
+     *            the resource resolver
+     * @return the builder for method chaining
+     */
+    public ResourceUriBuilder setResourceResolver(ResourceResolver resourceResolver) {
+        this.resourceResolver = resourceResolver;
+        return this;
+    }
+
+    /** Will take over scheme and authority (user info, host and port) from provided uri.
+     * 
+     * @param uri
+     * @return the builder for method chaining */
+    public ResourceUriBuilder useSchemeAndAuthority(URI uri) {
+        useSchemeAndAuthority(createFrom(uri, resourceResolver).build());
+        return this;
+    }
+
+    /** Builds the immutable ResourceUri from this builder.
+     * 
+     * @return the builder for method chaining */
+    public ResourceUri build() {
+        return new ImmutableResourceUri();
+    }
+
+    private String toStringInternal() {
+        StringBuilder requestUri = new StringBuilder();
+
+        if (StringUtils.isNotBlank(scheme)) {
+            requestUri.append(scheme + CHAR_COLON);
+        }
+        if (isNotBlank(scheme) && isNotBlank(host)) {
+            requestUri.append(CHAR_SLASH + CHAR_SLASH);
+            if (StringUtils.isNotBlank(userInfo)) {
+                requestUri.append(userInfo + CHAR_AT);
+            }
+            requestUri.append(host);
+            if (port > 0
+                    && !(scheme.equals(HTTP_SCHEME) && port == HTTP_DEFAULT_PORT)
+                    && !(scheme.equals(HTTPS_SCHEME) && port == HTTPS_DEFAULT_PORT)) {
+                requestUri.append(CHAR_COLON + port);
+            }
+        }
+        if (resourcePath != null) {
+            requestUri.append(assemblePath(true));
+        }
+        if (schemeSpecificPart != null) {
+            requestUri.append(schemeSpecificPart);
+        }
+        if (query != null) {
+            requestUri.append(CHAR_QM + query);
+        }
+        if (fragment != null) {
+            requestUri.append(CHAR_HASH + fragment);
+        }
+        return requestUri.toString();
+    }
+
+    /** @return string representation of builder */
+    public String toString() {
+        return toStringInternal();
+    }
+
+    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) {
+        Map<String, String> currentPathParameters = null;
+        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();
+                }
+                if (currentPathParameters == null) {
+                    currentPathParameters = new LinkedHashMap<>();
+                }
+                regexMatcher.appendReplacement(resultString, "");
+                String key = regexMatcher.group(1);
+                String value = StringUtils.defaultIfEmpty(regexMatcher.group(2), regexMatcher.group(3));
+                currentPathParameters.put(key, value);
+            }
+            if (resultString != null) {
+                regexMatcher.appendTail(resultString);
+                path = resultString.toString();
+                pathParameters.putAll(currentPathParameters);
+            }
+        }
+        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 (!StringUtils.isBlank(extension)) {
+            pathBuilder.append(CHAR_DOT + extension);
+            dotAdded = true;
+        }
+
+        if (!StringUtils.isBlank(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 ImmutableResourceUri implements ResourceUri {
+
+        @Override
+        public String getResourcePath() {
+            return resourcePath;
+        }
+
+        // returns null in line with
+        // https://sling.apache.org/apidocs/sling11/org/apache/sling/api/request/RequestPathInfo.html#getSelectorString--
+        @Override
+        public String getSelectorString() {
+            return !selectors.isEmpty() ? String.join(CHAR_DOT, selectors) : null;
+        }
+
+        @Override
+        public String[] getSelectors() {
+            return selectors.toArray(new String[selectors.size()]);
+        }
+
+        @Override
+        public String getExtension() {
+            return extension;
+        }
+
+        @Override
+        public Map<String, String> getPathParameters() {
+            return Collections.unmodifiableMap(pathParameters);
+        }
+
+        @Override
+        public String getSuffix() {
+            return suffix;
+        }
+
+        @Override
+        public String getPath() {
+            return assemblePath(true);
+        }
+
+        @Override
+        public String getSchemeSpecificPart() {
+            return schemeSpecificPart;
+        }
+
+        @Override
+        public String getQuery() {
+            return query;
+        }
+
+        @Override
+        public String getFragment() {
+            return fragment;
+        }
+
+        @Override
+        public String getScheme() {
+            return scheme;
+        }
+
+        @Override
+        public String getHost() {
+            return host;
+        }
+
+        @Override
+        public int getPort() {
+            return port;
+        }
+
+        @Override
+        public Resource getSuffixResource() {
+            if (StringUtils.isNotBlank(suffix) && resourceResolver != null) {
+                return resourceResolver.resolve(suffix);
+            } else {
+                return null;
+            }
+        }
+
+        @Override
+        public String getUserInfo() {
+            return userInfo;
+        }
+
+        @Override
+        public String toString() {
+            return toStringInternal();
+        }
+
+        @Override
+        public URI toUri() {
+            String uriString = toString();
+            try {
+                return new URI(uriString);
+            } catch (URISyntaxException e) {
+                throw new IllegalArgumentException("Invalid Sling URI: " + uriString, e);
+            }
+        }
+
+        private ResourceUriBuilder getBuilder() {
+            return ResourceUriBuilder.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 == null) ? 0 : 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 == null) ? 0 : 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;
+            ImmutableResourceUri other = (ImmutableResourceUri) obj;
+            if (extension == null) {
+                if (other.getBuilder().extension != null)
+                    return false;
+            } else if (!extension.equals(other.getBuilder().extension))
+                return false;
+            if (fragment == null) {
+                if (other.getBuilder().fragment != null)
+                    return false;
+            } else if (!fragment.equals(other.getBuilder().fragment))
+                return false;
+            if (host == null) {
+                if (other.getBuilder().host != null)
+                    return false;
+            } else if (!host.equals(other.getBuilder().host))
+                return false;
+            if (pathParameters == null) {
+                if (other.getBuilder().pathParameters != null)
+                    return false;
+            } else if (!pathParameters.equals(other.getBuilder().pathParameters))
+                return false;
+            if (port != other.getBuilder().port)
+                return false;
+            if (query == null) {
+                if (other.getBuilder().query != null)
+                    return false;
+            } else if (!query.equals(other.getBuilder().query))
+                return false;
+            if (resourcePath == null) {
+                if (other.getBuilder().resourcePath != null)
+                    return false;
+            } else if (!resourcePath.equals(other.getBuilder().resourcePath))
+                return false;
+            if (scheme == null) {
+                if (other.getBuilder().scheme != null)
+                    return false;
+            } else if (!scheme.equals(other.getBuilder().scheme))
+                return false;
+            if (schemeSpecificPart == null) {
+                if (other.getBuilder().schemeSpecificPart != null)
+                    return false;
+            } else if (!schemeSpecificPart.equals(other.getBuilder().schemeSpecificPart))
+                return false;
+            if (selectors == null) {
+                if (other.getBuilder().selectors != null)
+                    return false;
+            } else if (!selectors.equals(other.getBuilder().selectors))
+                return false;
+            if (suffix == null) {
+                if (other.getBuilder().suffix != null)
+                    return false;
+            } else if (!suffix.equals(other.getBuilder().suffix))
+                return false;
+            if (userInfo == null) {
+                if (other.getBuilder().userInfo != null)
+                    return false;
+            } else if (!userInfo.equals(other.getBuilder().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;
+        }
+
+        public void remove() {
+            throw new UnsupportedOperationException("remove");
+        }
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/api/resource/uri/package-info.java b/src/main/java/org/apache/sling/api/resource/uri/package-info.java
new file mode 100644
index 0000000..dd21e49
--- /dev/null
+++ b/src/main/java/org/apache/sling/api/resource/uri/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+@Version("1.0.0")
+package org.apache.sling.api.resource.uri;
+
+import org.osgi.annotation.versioning.Version;
+
diff --git a/src/test/java/org/apache/sling/api/resource/uri/ResourceUriBuilderTest.java b/src/test/java/org/apache/sling/api/resource/uri/ResourceUriBuilderTest.java
new file mode 100644
index 0000000..523b594
--- /dev/null
+++ b/src/test/java/org/apache/sling/api/resource/uri/ResourceUriBuilderTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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.resource.uri;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.when;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.request.RequestPathInfo;
+import org.apache.sling.api.resource.Resource;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ResourceUriBuilderTest {
+
+    @Mock
+    SlingHttpServletRequest request;
+
+    @Mock
+    RequestPathInfo requestPathInfo;
+
+    @Mock
+    Resource resource;
+
+    @Before
+    public void before() {
+        when(request.getRequestPathInfo()).thenReturn(requestPathInfo);
+    }
+
+    @Test
+    public void testBasicUsage() {
+
+        ResourceUri testUri = ResourceUriBuilder.create()
+                .setResourcePath("/test/to/path")
+                .setSelectors(new String[] { "sel1", "sel2" })
+                .setExtension("html")
+                .setSuffix("/suffix/path")
+                .setQuery("par1=val1&par2=val2")
+                .build();
+
+        assertEquals("/test/to/path.sel1.sel2.html/suffix/path?par1=val1&par2=val2", testUri.toString());
+    }
+
+    // the tests in ResourceUriTest extensively test the builder's parse method by using it for constructing
+    // all types of ResourceUris
+    @Test
+    public void testParse() {
+
+        String testUriStr = "https://example.com/test/to/path.sel1.sel2.html";
+        ResourceUri testUri = ResourceUriBuilder.parse(testUriStr, null).build();
+        assertEquals(testUriStr, testUri.toString());
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testParseInvalidUri() {
+        ResourceUriBuilder.parse(":foo", null);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testSetInvalidSuffix() {
+        ResourceUriBuilder.parse("/test/to/path.sel1.html", null).setSuffix("suffixWithoutSlash");
+    }
+
+    @Test
+    public void testCreateFromRequest() {
+
+        when(request.getScheme()).thenReturn("https");
+        when(request.getServerName()).thenReturn("example.com");
+        when(request.getServerPort()).thenReturn(443);
+        when(request.getQueryString()).thenReturn("par1=val1");
+        when(requestPathInfo.getResourcePath()).thenReturn("/test/to/path");
+        when(requestPathInfo.getSelectors()).thenReturn(new String[] { "sel1", "sel2" });
+        when(requestPathInfo.getExtension()).thenReturn("html");
+        when(requestPathInfo.getSuffix()).thenReturn("/suffix/path");
+
+        ResourceUri testUri = ResourceUriBuilder.createFrom(request).build();
+
+        assertEquals("https://example.com/test/to/path.sel1.sel2.html/suffix/path?par1=val1", testUri.toString());
+    }
+
+    @Test
+    public void testCreateFromResource() {
+
+        when(resource.getPath()).thenReturn("/test/to/path");
+        ResourceUri testUri = ResourceUriBuilder.createFrom(resource).build();
+
+        assertEquals("/test/to/path", testUri.getResourcePath());
+        assertNull(testUri.getSelectorString());
+        assertNull(testUri.getExtension());
+        assertNull(testUri.getSuffix());
+    }
+
+    @Test
+    public void testCreateFromResourceWithDotInPath() {
+
+        when(resource.getPath()).thenReturn("/test/to/image.jpg");
+        ResourceUri testUri = ResourceUriBuilder.createFrom(resource).build();
+
+        assertEquals("/test/to/image.jpg", testUri.getResourcePath());
+        assertNull(testUri.getSelectorString());
+        assertNull(testUri.getExtension());
+        assertNull(testUri.getSuffix());
+    }
+
+    @Test
+    public void testCreateFromPath() {
+
+        when(request.getScheme()).thenReturn("https");
+        when(request.getServerName()).thenReturn("example.com");
+        when(request.getServerPort()).thenReturn(443);
+        when(request.getQueryString()).thenReturn("par1=val1");
+        when(requestPathInfo.getResourcePath()).thenReturn("/test/to/path");
+        when(requestPathInfo.getSelectors()).thenReturn(new String[] { "sel1", "sel2" });
+        when(requestPathInfo.getExtension()).thenReturn("html");
+        when(requestPathInfo.getSuffix()).thenReturn("/suffix/path");
+
+        ResourceUri testUri = ResourceUriBuilder.createFrom(request).build();
+
+        assertEquals("https://example.com/test/to/path.sel1.sel2.html/suffix/path?par1=val1", testUri.toString());
+    }
+
+    @Test
+    public void testUseSchemeAndAuthority() throws URISyntaxException {
+
+        URI testUriToUseSchemeAndAuthorityFrom = new URI("https://example.com:8080/test/to/path.sel1.sel2.html");
+        String testPath = "/path/to/page.html";
+        ResourceUri testUri = ResourceUriBuilder.parse(testPath, null)
+                .useSchemeAndAuthority(testUriToUseSchemeAndAuthorityFrom)
+                .build();
+        assertEquals("https://example.com:8080/path/to/page.html", testUri.toString());
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/api/resource/uri/ResourceUriBuilderWithAdjustMethodTest.java b/src/test/java/org/apache/sling/api/resource/uri/ResourceUriBuilderWithAdjustMethodTest.java
new file mode 100644
index 0000000..87c233d
--- /dev/null
+++ b/src/test/java/org/apache/sling/api/resource/uri/ResourceUriBuilderWithAdjustMethodTest.java
@@ -0,0 +1,244 @@
+/*
+ * 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.resource.uri;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import java.util.function.Consumer;
+
+import org.apache.sling.api.resource.ResourceResolver;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ResourceUriBuilderWithAdjustMethodTest {
+
+    @Test
+    public void testAdjustAddSelectorFullUrl() {
+
+        testAdjustUri(
+                "http://host.com/test/to/path.html",
+                resourceUriBuilder -> {
+                    resourceUriBuilder.addSelector("test");
+                },
+                "http://host.com/test/to/path.test.html",
+                resourceUri -> {
+                    assertEquals("test", resourceUri.getSelectorString());
+                });
+    }
+
+    @Test
+    public void testAdjustAddSelectorAndSuffixPath() {
+
+        testAdjustUri(
+                "/test/to/path.html",
+                resourceUriBuilder -> {
+                    resourceUriBuilder.addSelector("test");
+                    resourceUriBuilder.setSuffix("/suffix/path/to/file");
+                },
+                "/test/to/path.test.html/suffix/path/to/file",
+                resourceUri -> {
+                    assertArrayEquals(new String[] { "test" }, resourceUri.getSelectors());
+                    assertEquals("/suffix/path/to/file", resourceUri.getSuffix());
+                });
+    }
+
+    @Test
+    public void testExtendSimplePathToFullUrl() {
+
+        testAdjustUri(
+                "/test/to/path.html",
+                resourceUriBuilder -> {
+                    resourceUriBuilder.setScheme("https");
+                    resourceUriBuilder.setHost("example.com");
+                    resourceUriBuilder.setSuffix("/suffix/path/to/file");
+                },
+                "https://example.com/test/to/path.html/suffix/path/to/file",
+                resourceUri -> {
+                    assertEquals("https", resourceUri.getScheme());
+                    assertEquals("example.com", resourceUri.getHost());
+                    assertEquals("/suffix/path/to/file", resourceUri.getSuffix());
+                });
+    }
+
+    @Test
+    public void testSetSelectorAndSuffixToRelativeUrl() {
+
+        testAdjustUri(
+                "../to/path.html",
+                resourceUriBuilder -> {
+                    resourceUriBuilder.addSelector("sel1");
+                    resourceUriBuilder.setSuffix("/suffix/path/to/file");
+                },
+                "../to/path.sel1.html/suffix/path/to/file",
+                resourceUri -> {
+                    assertEquals("../to/path", resourceUri.getResourcePath());
+                    assertEquals("sel1", resourceUri.getSelectorString());
+                    assertEquals("/suffix/path/to/file", resourceUri.getSuffix());
+                });
+    }
+
+    @Test
+    public void testFullUrltoSimplePath() {
+
+        testAdjustUri(
+                "https://user:pw@example.com/test/to/path.html/suffix/path/to/file",
+                resourceUriBuilder -> {
+                    resourceUriBuilder.removeSchemeAndAuthority();
+                },
+                "/test/to/path.html/suffix/path/to/file",
+                resourceUri -> {
+                    assertEquals(null, resourceUri.getScheme());
+                    assertEquals(null, resourceUri.getUserInfo());
+                    assertEquals(null, resourceUri.getHost());
+                });
+    }
+
+    @Test
+    public void testAdjustPathInOpaqueUriWithoutEffect() {
+
+        testAdjustUri(
+                "mailto:jon.doe@example.com",
+                resourceUriBuilder -> {
+                    resourceUriBuilder.setUserInfo("user:pw");
+                    resourceUriBuilder.setHost("example.com");
+                    resourceUriBuilder.setPort(500);
+                    resourceUriBuilder.setPath("/path/to/resource");
+                    resourceUriBuilder.setResourcePath("/path/to/resource");
+                    resourceUriBuilder.addSelector("test");
+                    resourceUriBuilder.setExtension("html");
+                    resourceUriBuilder.setSuffix("/suffix");
+                },
+                "mailto:jon.doe@example.com",
+                resourceUri -> {
+                    assertNull(resourceUri.getHost());
+                    assertNull(resourceUri.getResourcePath());
+                    assertNull(resourceUri.getSelectorString());
+                    assertNull(resourceUri.getExtension());
+                    assertNull(resourceUri.getSuffix());
+                });
+    }
+
+    @Test
+    public void testAdjustOpaqueToNormalUrl() {
+
+        testAdjustUri(
+                "mailto:jon.doe@example.com",
+                resourceUriBuilder -> {
+                    resourceUriBuilder.setSchemeSpecificPart(null);
+                    resourceUriBuilder.setScheme("https");
+                    resourceUriBuilder.setHost("example.com");
+                    resourceUriBuilder.setPath("/path/to/resource.html");
+                },
+                "https://example.com/path/to/resource.html",
+                resourceUri -> {
+                    assertNull(resourceUri.getSchemeSpecificPart());
+                });
+    }
+
+    @Test
+    public void testAdjustOpaqueUri() {
+
+        testAdjustUri(
+                "mailto:jon.doe@example.com",
+                resourceUriBuilder -> {
+                    resourceUriBuilder.setSchemeSpecificPart("mary.doe@example.com");
+                },
+                "mailto:mary.doe@example.com",
+                resourceUri -> {
+                    assertEquals("mary.doe@example.com", resourceUri.getSchemeSpecificPart());
+                    assertNull(resourceUri.getResourcePath());
+                });
+    }
+
+    @Test
+    public void testAdjustSelectorsInFragmentOnlyUrlWithoutEffect() {
+
+        testAdjustUri(
+                "#fragment",
+                resourceUriBuilder -> {
+                    resourceUriBuilder.addSelector("test");
+                    resourceUriBuilder.setSuffix("/suffix");
+                },
+                "#fragment",
+                resourceUri -> {
+                    assertEquals(null, resourceUri.getSelectorString());
+                    assertEquals(null, resourceUri.getSuffix());
+                });
+    }
+
+    @Test
+    public void testAjustFtpUrl() {
+
+        testAdjustUri(
+                "sftp://user:pw@example.com:9090/some/path",
+                resourceUriBuilder -> {
+                    resourceUriBuilder.setPath("/some/other/path");
+                    resourceUriBuilder.setPort(9091);
+                },
+                "sftp://user:pw@example.com:9091/some/other/path",
+                resourceUri -> {
+                    assertEquals("/some/other/path", resourceUri.getResourcePath());
+                    assertEquals(null, resourceUri.getSelectorString());
+                    assertEquals(9091, resourceUri.getPort());
+                });
+    }
+
+    @Test
+    public void testAdjustPathParameter() {
+
+        testAdjustUri(
+                "/test/to/path.sel1.html/suffix/path/to/file",
+                resourceUriBuilder -> {
+                    resourceUriBuilder.setPathParameter("v", "2.0");
+                },
+                "/test/to/path;v='2.0'.sel1.html/suffix/path/to/file",
+                resourceUri -> {
+                    assertEquals("/test/to/path", resourceUri.getResourcePath());
+                    assertEquals("sel1", resourceUri.getSelectorString());
+                    assertEquals("html", resourceUri.getExtension());
+                    assertEquals("/suffix/path/to/file", resourceUri.getSuffix());
+                    assertEquals(1, resourceUri.getPathParameters().size());
+                    assertEquals("2.0", resourceUri.getPathParameters().get("v"));
+                });
+    }
+
+    // -- helper methods
+
+    public static void testAdjustUri(String testUri, Consumer<ResourceUriBuilder> adjuster, String testUriAfterEdit,
+            Consumer<ResourceUri> additionalAssertions) {
+        testAdjustUri(testUri, adjuster, testUriAfterEdit, additionalAssertions, null);
+    }
+
+    public static void testAdjustUri(String testUri, Consumer<ResourceUriBuilder> adjuster, String testUriAfterEdit,
+            Consumer<ResourceUri> additionalAssertions, ResourceResolver resourceResolver) {
+        ResourceUri resourceUri = ResourceUriBuilder.parse(testUri, resourceResolver).build();
+
+        ResourceUri adjustedResourceUri = resourceUri.adjust(adjuster);
+
+        assertEquals(testUriAfterEdit, adjustedResourceUri.toString());
+        assertEquals(testUriAfterEdit, adjustedResourceUri.toUri().toString());
+
+        additionalAssertions.accept(adjustedResourceUri);
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/api/resource/uri/ResourceUriRebaseTest.java b/src/test/java/org/apache/sling/api/resource/uri/ResourceUriRebaseTest.java
new file mode 100644
index 0000000..ea2e986
--- /dev/null
+++ b/src/test/java/org/apache/sling/api/resource/uri/ResourceUriRebaseTest.java
@@ -0,0 +1,231 @@
+/*
+ * 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.resource.uri;
+
+import static org.apache.sling.api.resource.uri.ResourceUriTest.testUri;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.when;
+
+import java.net.URISyntaxException;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.Silent.class)
+public class ResourceUriRebaseTest {
+
+    private static final String FULL_URI = "/test/to/file.ext.sel1.json/suffix/path.js";
+
+    @Mock
+    ResourceResolver resolver;
+
+    @Mock
+    Resource resource;
+
+    @Test
+    public void testRebaseResourcePathSimplePath() {
+        String testUriStrSimple = "/test/to/file";
+        when(resolver.getResource("/test/to/file")).thenReturn(resource);
+        testUri(testUriStrSimple, true, true, false, false, false, resourceUri -> {
+            assertEquals("/test/to/file", resourceUri.getResourcePath());
+            assertEquals(null, resourceUri.getSelectorString());
+            assertEquals(null, resourceUri.getExtension());
+            assertEquals(null, resourceUri.getSuffix());
+        }, resolver);
+    }
+
+    @Test
+    public void testRebaseResourcePathSimpleResourcePathIncludesExtension() {
+
+        String testUriStrSimpleFile = "/test/to/file.css";
+        when(resolver.getResource("/test/to/file.css")).thenReturn(resource);
+        when(resolver.getResource("/test/to/file")).thenReturn(null);
+        testUri(testUriStrSimpleFile, true, true, false, false, false, resourceUri -> {
+            assertEquals("/test/to/file.css", resourceUri.getResourcePath());
+            assertEquals(null, resourceUri.getSelectorString());
+            assertEquals(null, resourceUri.getExtension());
+            assertEquals(null, resourceUri.getSuffix());
+        }, resolver);
+    }
+
+    @Test
+    public void testRebaseResourcePathSimpleResourcePathExcludesExtension() {
+        String testUriStrSimplePage = "/path/to/page.html";
+        when(resolver.getResource("/path/to/page.html")).thenReturn(null);
+        when(resolver.getResource("/path/to/page")).thenReturn(resource);
+        testUri(testUriStrSimplePage, true, true, false, false, false, resourceUri -> {
+            assertEquals("/path/to/page", resourceUri.getResourcePath());
+            assertEquals(null, resourceUri.getSelectorString());
+            assertEquals("html", resourceUri.getExtension());
+            assertEquals(null, resourceUri.getSuffix());
+        }, resolver);
+    }
+
+    @Test
+    public void testRebaseResourcePathFullPathIsResource() {
+
+        // pull path with suffix is resource path
+        when(resolver.getResource("/test/to/file.ext.sel1.json/suffix/path.js")).thenReturn(resource);
+        when(resolver.getResource("/test/to/file.ext.sel1.json/suffix/path")).thenReturn(null);
+        when(resolver.getResource("/test/to/file.ext.sel1.json")).thenReturn(null);
+        when(resolver.getResource("/test/to/file.ext.sel1")).thenReturn(null);
+        when(resolver.getResource("/test/to/file.ext")).thenReturn(null);
+        when(resolver.getResource("/test/to/file")).thenReturn(null);
+        testUri(FULL_URI, true, true, false, false, false, resourceUri -> {
+            assertEquals("/test/to/file.ext.sel1.json/suffix/path.js", resourceUri.getResourcePath());
+            assertEquals(null, resourceUri.getSelectorString());
+            assertEquals(null, resourceUri.getExtension());
+            assertEquals(null, resourceUri.getSuffix());
+        }, resolver);
+    }
+
+    @Test
+    public void testRebaseResourcePathPartOfSuffixIsResource() {
+        //
+        when(resolver.getResource("/test/to/file.ext.sel1.json/suffix/path.js")).thenReturn(null);
+        when(resolver.getResource("/test/to/file.ext.sel1.json/suffix/path")).thenReturn(resource);
+        when(resolver.getResource("/test/to/file.ext.sel1.json")).thenReturn(null);
+        when(resolver.getResource("/test/to/file.ext.sel1")).thenReturn(null);
+        when(resolver.getResource("/test/to/file.ext")).thenReturn(null);
+        when(resolver.getResource("/test/to/file")).thenReturn(null);
+        testUri(FULL_URI, true, true, false, false, false, resourceUri -> {
+            assertEquals("/test/to/file.ext.sel1.json/suffix/path", resourceUri.getResourcePath());
+            assertEquals(null, resourceUri.getSelectorString());
+            assertEquals("js", resourceUri.getExtension());
+            assertEquals(null, resourceUri.getSuffix());
+        }, resolver);
+    }
+
+    @Test
+    public void testRebaseResourcePathWithSelectorsAndExtension() {
+        // if the resource for before the suffix part exits, it is ignored
+        // compare org.apache.sling.resourceresolver.impl.helper.ResourcePathIteratorTest.testMixed()
+        when(resolver.getResource("/test/to/file.ext.sel1.json/suffix/path.js")).thenReturn(null);
+        when(resolver.getResource("/test/to/file.ext.sel1.json/suffix/path")).thenReturn(null);
+        when(resolver.getResource("/test/to/file.ext.sel1.json")).thenReturn(resource);
+        when(resolver.getResource("/test/to/file.ext.sel1")).thenReturn(null);
+        when(resolver.getResource("/test/to/file.ext")).thenReturn(null);
+        when(resolver.getResource("/test/to/file")).thenReturn(null);
+        testUri(FULL_URI, true, true, false, false, false, resourceUri -> {
+            assertEquals("/test/to/file", resourceUri.getResourcePath());
+            assertEquals("ext.sel1", resourceUri.getSelectorString());
+            assertEquals("json", resourceUri.getExtension());
+            assertEquals("/suffix/path.js", resourceUri.getSuffix());
+        }, resolver);
+
+    }
+
+    @Test
+    public void testRebaseResourcePathContainsTwoDots() {
+        when(resolver.getResource("/test/to/file.ext.sel1.json/suffix/path.js")).thenReturn(null);
+        when(resolver.getResource("/test/to/file.ext.sel1.json/suffix/path")).thenReturn(null);
+        when(resolver.getResource("/test/to/file.ext.sel1.json")).thenReturn(null);
+        when(resolver.getResource("/test/to/file.ext.sel1")).thenReturn(resource);
+        when(resolver.getResource("/test/to/file.ext")).thenReturn(null);
+        when(resolver.getResource("/test/to/file")).thenReturn(null);
+        testUri(FULL_URI, true, true, false, false, false, resourceUri -> {
+            assertEquals("/test/to/file.ext.sel1", resourceUri.getResourcePath());
+            assertEquals(null, resourceUri.getSelectorString());
+            assertEquals("json", resourceUri.getExtension());
+            assertEquals("/suffix/path.js", resourceUri.getSuffix());
+        }, resolver);
+
+    }
+
+    @Test
+    public void testRebaseResourcePathContainsExtension() {
+        when(resolver.getResource("/test/to/file.ext.sel1.json/suffix/path.js")).thenReturn(null);
+        when(resolver.getResource("/test/to/file.ext.sel1.json/suffix/path")).thenReturn(null);
+        when(resolver.getResource("/test/to/file.ext.sel1.json")).thenReturn(null);
+        when(resolver.getResource("/test/to/file.ext.sel1")).thenReturn(null);
+        when(resolver.getResource("/test/to/file.ext")).thenReturn(resource);
+        when(resolver.getResource("/test/to/file")).thenReturn(null);
+        testUri(FULL_URI, true, true, false, false, false, resourceUri -> {
+            assertEquals("/test/to/file.ext", resourceUri.getResourcePath());
+            assertEquals("sel1", resourceUri.getSelectorString());
+            assertEquals("json", resourceUri.getExtension());
+            assertEquals("/suffix/path.js", resourceUri.getSuffix());
+        }, resolver);
+
+    }
+
+    @Test
+    public void testRebaseResourcePathWithoutDot() {
+        when(resolver.getResource("/test/to/file.ext.sel1.json/suffix/path.js")).thenReturn(null);
+        when(resolver.getResource("/test/to/file.ext.sel1.json/suffix/path")).thenReturn(null);
+        when(resolver.getResource("/test/to/file.ext.sel1.json")).thenReturn(null);
+        when(resolver.getResource("/test/to/file.ext.sel1")).thenReturn(null);
+        when(resolver.getResource("/test/to/file.ext")).thenReturn(null);
+        when(resolver.getResource("/test/to/file")).thenReturn(resource);
+        testUri(FULL_URI, true, true, false, false, false, resourceUri -> {
+            assertEquals("/test/to/file", resourceUri.getResourcePath());
+            assertEquals("ext.sel1", resourceUri.getSelectorString());
+            assertEquals("json", resourceUri.getExtension());
+            assertEquals("/suffix/path.js", resourceUri.getSuffix());
+        }, resolver);
+    }
+
+    @Test
+    public void testRebaseResourcePathLongestMatchingPathWins() {
+        // side by side resources in same folder: the longest path wins
+        when(resolver.getResource("/test/to/file.ext.sel1.json/suffix/path.js")).thenReturn(null);
+        when(resolver.getResource("/test/to/file.ext.sel1.json/suffix/path")).thenReturn(null);
+        when(resolver.getResource("/test/to/file.ext.sel1.json")).thenReturn(null);
+        when(resolver.getResource("/test/to/file.ext.sel1")).thenReturn(resource);
+        when(resolver.getResource("/test/to/file.ext")).thenReturn(resource);
+        when(resolver.getResource("/test/to/file")).thenReturn(resource);
+        testUri(FULL_URI, true, true, false, false, false, resourceUri -> {
+            assertEquals("/test/to/file.ext.sel1", resourceUri.getResourcePath());
+            assertEquals(null, resourceUri.getSelectorString());
+            assertEquals("json", resourceUri.getExtension());
+            assertEquals("/suffix/path.js", resourceUri.getSuffix());
+        }, resolver);
+    }
+
+    @Test
+    public void testRebaseResourcePathNoResourceExistsAtAll() {
+        // if no resource exists at all the first dot has to be taken as split
+        when(resolver.getResource("/test/to/file.ext.sel1.json/suffix/path.js")).thenReturn(null);
+        when(resolver.getResource("/test/to/file.ext.sel1.json/suffix/path")).thenReturn(null);
+        when(resolver.getResource("/test/to/file.ext.sel1.json")).thenReturn(null);
+        when(resolver.getResource("/test/to/file.ext.sel1")).thenReturn(null);
+        when(resolver.getResource("/test/to/file.ext")).thenReturn(null);
+        when(resolver.getResource("/test/to/file")).thenReturn(null);
+        testUri(FULL_URI, true, true, false, false, false, resourceUri -> {
+            assertEquals("/test/to/file", resourceUri.getResourcePath());
+            assertEquals("ext.sel1", resourceUri.getSelectorString());
+            assertEquals("json", resourceUri.getExtension());
+            assertEquals("/suffix/path.js", resourceUri.getSuffix());
+        }, resolver);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testRebaseNotAllowedWithoutResolver() throws URISyntaxException {
+
+        String testPath = "/path/to/page.html";
+        ResourceUriBuilder.parse(testPath, null)
+                .rebaseResourcePath()
+                .build();
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/api/resource/uri/ResourceUriTest.java b/src/test/java/org/apache/sling/api/resource/uri/ResourceUriTest.java
new file mode 100644
index 0000000..18cd6e3
--- /dev/null
+++ b/src/test/java/org/apache/sling/api/resource/uri/ResourceUriTest.java
@@ -0,0 +1,442 @@
+/*
+ * 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.resource.uri;
+
+import static java.util.Arrays.asList;
+import static org.junit.Assert.assertEquals;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+import org.apache.sling.api.resource.ResourceResolver;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ResourceUriTest {
+
+    @Mock
+    ResourceResolver resolver;
+
+    @Test
+    public void testFullResourceUri() {
+
+        String testUriStr = "http://host.com/test/to/path.html";
+        testUri(testUriStr, false, false, false, true, false, resourceUri -> {
+            assertEquals("http", resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals("host.com", resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals("/test/to/path", resourceUri.getResourcePath());
+            assertEquals(null, resourceUri.getSelectorString());
+            assertEquals("html", resourceUri.getExtension());
+            assertEquals(null, resourceUri.getSuffix());
+            assertEquals(null, resourceUri.getSchemeSpecificPart());
+            assertEquals(null, resourceUri.getQuery());
+            assertEquals(null, resourceUri.getFragment());
+        }, asList(resolver, null));
+
+    }
+
+    @Test
+    public void testFullResourceUriComplex() {
+
+        String testUriStr = "https://test:pw@host.com:888/test/to/path.sel1.json/suffix/path?p1=2&p2=3#frag3939";
+        testUri(testUriStr, false, false, false, true, false, resourceUri -> {
+            assertEquals("https", resourceUri.getScheme());
+            assertEquals("test:pw", resourceUri.getUserInfo());
+            assertEquals("host.com", resourceUri.getHost());
+            assertEquals(888, resourceUri.getPort());
+            assertEquals("/test/to/path", resourceUri.getResourcePath());
+            assertEquals("sel1", resourceUri.getSelectorString());
+            assertEquals("json", resourceUri.getExtension());
+            assertEquals("/suffix/path", resourceUri.getSuffix());
+            assertEquals(null, resourceUri.getSchemeSpecificPart());
+            assertEquals("p1=2&p2=3", resourceUri.getQuery());
+            assertEquals("frag3939", resourceUri.getFragment());
+        }, asList(resolver, null));
+
+    }
+
+    @Test
+    public void testAbsolutePathResourceUri() {
+        String testUriStr = "/test/to/path.sel1.json/suffix/path?p1=2&p2=3#frag3939";
+
+        testUri(testUriStr, true, true, false, false, false, resourceUri -> {
+            assertEquals(null, resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals(null, resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals("/test/to/path", resourceUri.getResourcePath());
+            assertEquals("sel1", resourceUri.getSelectorString());
+            assertEquals("json", resourceUri.getExtension());
+            assertEquals("/suffix/path", resourceUri.getSuffix());
+            assertEquals(null, resourceUri.getSchemeSpecificPart());
+            assertEquals("p1=2&p2=3", resourceUri.getQuery());
+            assertEquals("frag3939", resourceUri.getFragment());
+        }, asList(resolver, null));
+    }
+
+    @Test
+    public void testResourceUriSuffixWithDots() {
+
+        String testUriStr = "/test/to/path.min.js/suffix/app.nodesbrowser.js";
+        testUri(testUriStr, true, true, false, false, false, resourceUri -> {
+            assertEquals(null, resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals(null, resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals("/test/to/path", resourceUri.getResourcePath());
+            assertEquals("min", resourceUri.getSelectorString());
+            assertEquals("js", resourceUri.getExtension());
+            assertEquals("/suffix/app.nodesbrowser.js", resourceUri.getSuffix());
+            assertEquals(null, resourceUri.getSchemeSpecificPart());
+            assertEquals(null, resourceUri.getQuery());
+            assertEquals(null, resourceUri.getFragment());
+        }, asList(resolver, null));
+    }
+
+    @Test
+    public void testResourceUriMultipleDots() {
+
+        String testUriStr = "/test/to/path.sel1.sel2..sel4.js";
+        testUri(testUriStr, true, true, false, false, false, resourceUri -> {
+            assertEquals(null, resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals(null, resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals("/test/to/path", resourceUri.getResourcePath());
+            assertEquals(4, resourceUri.getSelectors().length);
+            assertEquals("sel1.sel2..sel4", resourceUri.getSelectorString());
+            assertEquals("js", resourceUri.getExtension());
+            assertEquals(null, resourceUri.getSuffix());
+            assertEquals(null, resourceUri.getSchemeSpecificPart());
+            assertEquals(null, resourceUri.getQuery());
+            assertEquals(null, resourceUri.getFragment());
+        }, asList(resolver, null));
+
+        String testUriStr2 = "/test/to/path.sel1.sel2../sel4.js";
+        testUri(testUriStr2, true, true, false, false, false, resourceUri -> {
+            assertEquals(null, resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals(null, resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals("/test/to/path", resourceUri.getResourcePath());
+            assertEquals(1, resourceUri.getSelectors().length);
+            assertEquals("sel1", resourceUri.getSelectorString());
+            assertEquals("sel2", resourceUri.getExtension());
+            assertEquals("/sel4.js", resourceUri.getSuffix());
+            assertEquals(null, resourceUri.getSchemeSpecificPart());
+            assertEquals(null, resourceUri.getQuery());
+            assertEquals(null, resourceUri.getFragment());
+        }, null, true);
+    }
+
+    @Test
+    public void testRelativePathResourceUri() {
+        String testUriStr = "../path.html#frag1";
+
+        testUri(testUriStr, true, false, true, false, false, resourceUri -> {
+            assertEquals(null, resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals(null, resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals("../path", resourceUri.getResourcePath());
+            assertEquals(null, resourceUri.getSelectorString());
+            assertEquals("html", resourceUri.getExtension());
+            assertEquals(null, resourceUri.getSuffix());
+            assertEquals(null, resourceUri.getSchemeSpecificPart());
+            assertEquals(null, resourceUri.getQuery());
+            assertEquals("frag1", resourceUri.getFragment());
+        }, asList(resolver, null));
+    }
+
+    @Test
+    public void testRelativePathResourceUriComplex() {
+        String testUriStr = "../path/./deep/path/../path.sel1.sel2.html?test=1#frag1";
+
+        testUri(testUriStr, true, false, true, false, false, resourceUri -> {
+            assertEquals(null, resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals(null, resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals("../path/./deep/path/../path", resourceUri.getResourcePath());
+            assertEquals("sel1.sel2", resourceUri.getSelectorString());
+            assertEquals("html", resourceUri.getExtension());
+            assertEquals(null, resourceUri.getSuffix());
+            assertEquals(null, resourceUri.getSchemeSpecificPart());
+            assertEquals("test=1", resourceUri.getQuery());
+            assertEquals("frag1", resourceUri.getFragment());
+        }, asList(resolver, null));
+    }
+
+    @Test
+    public void testAbsolutePathWithPathParameter() {
+        String testUriStr = "/test/to/path;v='1.0'.sel1.html/suffix/path?p1=2&p2=3#frag3939";
+
+        testUri(testUriStr, true, true, false, false, false, resourceUri -> {
+            assertEquals(null, resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals(null, resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals("/test/to/path", resourceUri.getResourcePath());
+            assertEquals("sel1", resourceUri.getSelectorString());
+            assertEquals("html", resourceUri.getExtension());
+            assertEquals(1, resourceUri.getPathParameters().size());
+            assertEquals("1.0", resourceUri.getPathParameters().get("v"));
+            assertEquals("/suffix/path", resourceUri.getSuffix());
+            assertEquals(null, resourceUri.getSchemeSpecificPart());
+            assertEquals("p1=2&p2=3", resourceUri.getQuery());
+            assertEquals("frag3939", resourceUri.getFragment());
+        }, asList(resolver, null));
+
+        String testUriStr2 = "/test/to/file;foo='bar'.sel1.sel2.json/suffix/path";
+        testUri(testUriStr2, true, true, false, false, false, resourceUri -> {
+            assertEquals(null, resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals(null, resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals("/test/to/file", resourceUri.getResourcePath());
+            assertEquals("sel1.sel2", resourceUri.getSelectorString());
+            assertEquals("json", resourceUri.getExtension());
+            assertEquals(1, resourceUri.getPathParameters().size());
+            assertEquals("bar", resourceUri.getPathParameters().get("foo"));
+            assertEquals("/suffix/path", resourceUri.getSuffix());
+            assertEquals(null, resourceUri.getSchemeSpecificPart());
+            assertEquals(null, resourceUri.getQuery());
+            assertEquals(null, resourceUri.getFragment());
+        }, asList(resolver, null));
+    }
+
+    @Test
+    public void testAbsolutePathWithPathParameterMultiple() {
+        String testUriStr = "/test/to/path;v='1.0';antotherParam='test/nested';antotherParam2='7'.sel1.html/suffix/path?p1=2&p2=3#frag3939";
+
+        testUri(testUriStr, true, true, false, false, false, resourceUri -> {
+            assertEquals(null, resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals(null, resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals("/test/to/path", resourceUri.getResourcePath());
+            assertEquals("sel1", resourceUri.getSelectorString());
+            assertEquals("html", resourceUri.getExtension());
+
+            assertEquals(3, resourceUri.getPathParameters().size());
+            assertEquals("1.0", resourceUri.getPathParameters().get("v"));
+            assertEquals("test/nested", resourceUri.getPathParameters().get("antotherParam"));
+            assertEquals("7", resourceUri.getPathParameters().get("antotherParam2"));
+
+            assertEquals("/suffix/path", resourceUri.getSuffix());
+            assertEquals(null, resourceUri.getSchemeSpecificPart());
+            assertEquals("p1=2&p2=3", resourceUri.getQuery());
+            assertEquals("frag3939", resourceUri.getFragment());
+        }, asList(resolver, null));
+    }
+
+    @Test
+    public void testAbsolutePathWithPathParameterAfterExtension() {
+        String testUriStr = "/test/to/path.sel1.html;v='1.0'/suffix/path?p1=2&p2=3#frag3939";
+
+        ResourceUri testUri = testUri(testUriStr, true, true, false, false, false, resourceUri -> {
+            assertEquals(null, resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals(null, resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals("/test/to/path", resourceUri.getResourcePath());
+            assertEquals("sel1", resourceUri.getSelectorString());
+            assertEquals("html", resourceUri.getExtension());
+            assertEquals(1, resourceUri.getPathParameters().size());
+            assertEquals("1.0", resourceUri.getPathParameters().get("v"));
+            assertEquals("/suffix/path", resourceUri.getSuffix());
+            assertEquals(null, resourceUri.getSchemeSpecificPart());
+            assertEquals("p1=2&p2=3", resourceUri.getQuery());
+            assertEquals("frag3939", resourceUri.getFragment());
+        }, null, true /* URL is restructured (parameter moved to end), assertion below */);
+
+        assertEquals("/test/to/path;v='1.0'.sel1.html/suffix/path?p1=2&p2=3#frag3939", testUri.toString());
+
+    }
+
+    @Test
+    public void testJavascriptUri() {
+        String testUriStr = "javascript:void(0)";
+
+        testUri(testUriStr, false, false, false, false, true, resourceUri -> {
+            assertEquals("javascript", resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals(null, resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals(null, resourceUri.getSelectorString());
+            assertEquals(null, resourceUri.getExtension());
+            assertEquals(null, resourceUri.getSuffix());
+            assertEquals("void(0)", resourceUri.getSchemeSpecificPart());
+            assertEquals(null, resourceUri.getQuery());
+            assertEquals(null, resourceUri.getFragment());
+        }, asList(resolver, null));
+    }
+
+    @Test
+    public void testMailtotUri() {
+        String testUriStr = "mailto:jon.doe@example.com";
+
+        testUri(testUriStr, false, false, false, false, true, resourceUri -> {
+            assertEquals("mailto", resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals(null, resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals(null, resourceUri.getSelectorString());
+            assertEquals(null, resourceUri.getExtension());
+            assertEquals(null, resourceUri.getSuffix());
+            assertEquals("jon.doe@example.com", resourceUri.getSchemeSpecificPart());
+            assertEquals(null, resourceUri.getQuery());
+            assertEquals(null, resourceUri.getFragment());
+        }, asList(resolver, null));
+    }
+
+    @Test
+    public void testHashOnlyUri() {
+
+        testUri("#", false, false, false, false, false, resourceUri -> {
+            assertEquals(null, resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals(null, resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals(null, resourceUri.getResourcePath());
+            assertEquals(null, resourceUri.getSelectorString());
+            assertEquals(null, resourceUri.getExtension());
+            assertEquals(null, resourceUri.getSuffix());
+            assertEquals(null, resourceUri.getSchemeSpecificPart());
+            assertEquals(null, resourceUri.getQuery());
+            assertEquals("", resourceUri.getFragment());
+        }, asList(resolver, null));
+
+        testUri("#fragment", false, false, false, false, false, resourceUri -> {
+            assertEquals(null, resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals(null, resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals(null, resourceUri.getResourcePath());
+            assertEquals(null, resourceUri.getSelectorString());
+            assertEquals(null, resourceUri.getExtension());
+            assertEquals(null, resourceUri.getSuffix());
+            assertEquals(null, resourceUri.getSchemeSpecificPart());
+            assertEquals(null, resourceUri.getQuery());
+            assertEquals("fragment", resourceUri.getFragment());
+        }, asList(resolver, null));
+    }
+
+    @Test
+    public void testQueryOnlyUri() {
+
+        testUri("?", false, false, false, false, false, resourceUri -> {
+            assertEquals(null, resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals(null, resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals(null, resourceUri.getResourcePath());
+            assertEquals(null, resourceUri.getSelectorString());
+            assertEquals(null, resourceUri.getExtension());
+            assertEquals(null, resourceUri.getSuffix());
+            assertEquals(null, resourceUri.getSchemeSpecificPart());
+            assertEquals("", resourceUri.getQuery());
+            assertEquals(null, resourceUri.getFragment());
+        }, asList(resolver, null));
+
+        testUri("?test=test", false, false, false, false, false, resourceUri -> {
+            assertEquals(null, resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals(null, resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals(null, resourceUri.getResourcePath());
+            assertEquals(null, resourceUri.getSelectorString());
+            assertEquals(null, resourceUri.getExtension());
+            assertEquals(null, resourceUri.getSuffix());
+            assertEquals(null, resourceUri.getSchemeSpecificPart());
+            assertEquals("test=test", resourceUri.getQuery());
+            assertEquals(null, resourceUri.getFragment());
+        }, asList(resolver, null));
+    }
+
+    @Test
+    public void testUnusualQueryFragmentCombinations() {
+        testUri("?#", false, false, false, false, false, resourceUri -> {
+            assertEquals("", resourceUri.getQuery());
+            assertEquals("", resourceUri.getFragment());
+        }, asList(resolver, null));
+        testUri("?t=2#", false, false, false, false, false, resourceUri -> {
+            assertEquals("t=2", resourceUri.getQuery());
+            assertEquals("", resourceUri.getFragment());
+        }, asList(resolver, null));
+        testUri("?#t=3", false, false, false, false, false, resourceUri -> {
+            assertEquals("", resourceUri.getQuery());
+            assertEquals("t=3", resourceUri.getFragment());
+        }, asList(resolver, null));
+        testUri("", false, false, false, false, false, resourceUri -> {
+            assertEquals(null, resourceUri.getQuery());
+            assertEquals(null, resourceUri.getFragment());
+        }, asList(resolver, null));
+    }
+
+    // -- helper methods
+    public static void testUri(String testUri, boolean isPath, boolean isAbsolutePath, boolean isRelativePath, boolean isFullUri,
+            boolean isOpaque, Consumer<ResourceUri> additionalAssertions, List<ResourceResolver> resourceResolvers) {
+        for (ResourceResolver rr : resourceResolvers) {
+            testUri(testUri, isPath, isAbsolutePath, isRelativePath, isFullUri, isOpaque, additionalAssertions, rr);
+        }
+    }
+
+    public static ResourceUri testUri(String testUri, boolean isPath, boolean isAbsolutePath, boolean isRelativePath, boolean isFullUri,
+            boolean isOpaque, Consumer<ResourceUri> additionalAssertions) {
+        return testUri(testUri, isPath, isAbsolutePath, isRelativePath, isFullUri, isOpaque, additionalAssertions, (ResourceResolver) null);
+    }
+
+    public static ResourceUri testUri(String testUri, boolean isPath, boolean isAbsolutePath, boolean isRelativePath, boolean isFullUri,
+            boolean isOpaque, Consumer<ResourceUri> additionalAssertions, ResourceResolver resourceResolver) {
+        return testUri(testUri, isPath, isAbsolutePath, isRelativePath, isFullUri, isOpaque, additionalAssertions, resourceResolver, false);
+    }
+
+    public static ResourceUri testUri(String testUri, boolean isPath, boolean isAbsolutePath, boolean isRelativePath, boolean isFullUri,
+            boolean isOpaque, Consumer<ResourceUri> additionalAssertions, ResourceResolver resourceResolver, boolean urlIsRestructured) {
+        ResourceUri resourceUri = ResourceUriBuilder.parse(testUri, resourceResolver).build();
+
+        if (!urlIsRestructured) {
+            assertEquals("Uri toString() same as input", testUri, resourceUri.toString());
+            assertEquals("Uri toUri().toString() same as input", testUri, resourceUri.toUri().toString());
+        }
+
+        assertEquals("isPath()", isPath, resourceUri.isPath());
+        assertEquals("isAbsolutePath()", isAbsolutePath, resourceUri.isAbsolutePath());
+        assertEquals("isRelativePath()", isRelativePath, resourceUri.isRelativePath());
+        assertEquals("isFullUri()", isFullUri, resourceUri.isFullUri());
+        assertEquals("isOpaque()", isOpaque, resourceUri.isOpaque());
+        assertEquals("isOpaque() matches to java URI impl", resourceUri.toUri().isOpaque(), resourceUri.isOpaque());
+
+        additionalAssertions.accept(resourceUri);
+
+        ResourceUri resourceUriParsedFromSameInput = ResourceUriBuilder.parse(testUri, resourceResolver).build();
+        assertEquals("uris parsed from same input are expected to be equal", resourceUriParsedFromSameInput, resourceUri);
+        assertEquals("uris parsed from same input are expected to have the same hash code", resourceUriParsedFromSameInput.hashCode(),
+                resourceUri.hashCode());
+
+        return resourceUri;
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/api/resource/uri/ResourceUriToSlingRequestPathInfoCompatibilityTest.java b/src/test/java/org/apache/sling/api/resource/uri/ResourceUriToSlingRequestPathInfoCompatibilityTest.java
new file mode 100644
index 0000000..00ba823
--- /dev/null
+++ b/src/test/java/org/apache/sling/api/resource/uri/ResourceUriToSlingRequestPathInfoCompatibilityTest.java
@@ -0,0 +1,272 @@
+/*
+ * 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.resource.uri;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.when;
+
+import org.apache.sling.api.request.RequestPathInfo;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+// Replicates the same test cases as there are in SlingRequestPathInfoTest (from sling engine)
+// to ensure compatibility
+@RunWith(MockitoJUnitRunner.class)
+public class ResourceUriToSlingRequestPathInfoCompatibilityTest {
+
+    @Mock
+    ResourceResolver resourceResolver;
+
+    @Mock
+    Resource resource;
+
+    private RequestPathInfo createResourceUri(String resolutionPath, String resolutionPathInfo) {
+        when(resourceResolver.getResource(resolutionPath)).thenReturn(resource);
+        return ResourceUriBuilder.parse(resolutionPath + (resolutionPathInfo != null ? resolutionPathInfo : ""), resourceResolver).build();
+    }
+
+    @Test
+    public void testTrailingDot() {
+        RequestPathInfo p = createResourceUri("/some/path", ".");
+        assertEquals("/some/path", p.getResourcePath());
+        assertNull("Selectors are null", p.getSelectorString());
+        assertEquals(0, p.getSelectors().length);
+        assertNull("Extension is null", p.getExtension());
+        assertNull("Suffix is null", p.getSuffix());
+    }
+
+    @Test
+    public void testTrailingDotWithSuffix() {
+        RequestPathInfo p = createResourceUri("/some/path", "./suffix");
+        assertEquals("/some/path", p.getResourcePath());
+        assertNull("Selectors are null", p.getSelectorString());
+        assertEquals(0, p.getSelectors().length);
+        assertNull("Extension is null", p.getExtension());
+        assertEquals("/suffix", p.getSuffix());
+    }
+
+    @Test
+    public void testTrailingDotDot() {
+        RequestPathInfo p = createResourceUri("/some/path", "..");
+        assertEquals("/some/path", p.getResourcePath());
+        assertNull("Selectors are null", p.getSelectorString());
+        assertEquals(0, p.getSelectors().length);
+        assertNull("Extension is null", p.getExtension());
+        assertNull("Suffix is null", p.getSuffix());
+    }
+
+    @Test
+    public void testTrailingDotDotWithSuffix() {
+        RequestPathInfo p = createResourceUri("/some/path", "../suffix");
+        assertEquals("/some/path", p.getResourcePath());
+        assertNull("Selectors are null", p.getSelectorString());
+        assertEquals(0, p.getSelectors().length);
+        assertNull("Extension is null", p.getExtension());
+        assertEquals("/suffix", p.getSuffix());
+    }
+
+    @Test
+    public void testTrailingDotDotDot() {
+        RequestPathInfo p = createResourceUri("/some/path", "...");
+        assertEquals("/some/path", p.getResourcePath());
+        assertNull("Selectors are null", p.getSelectorString());
+        assertEquals(0, p.getSelectors().length);
+        assertNull("Extension is null", p.getExtension());
+        assertNull("Suffix is null", p.getSuffix());
+    }
+
+    @Test
+    public void testTrailingDotDotDotWithSuffix() {
+        RequestPathInfo p = createResourceUri("/some/path", ".../suffix");
+        assertEquals("/some/path", p.getResourcePath());
+        assertNull("Selectors are null", p.getSelectorString());
+        assertEquals(0, p.getSelectors().length);
+        assertNull("Extension is null", p.getExtension());
+        assertEquals("/suffix", p.getSuffix());
+        // the path changes slightly, the '.' is needed to still mark the suffix as suffix
+        assertEquals("/some/path./suffix", p.toString());
+    }
+
+    @Test
+    public void testAllOptions() {
+        RequestPathInfo p = createResourceUri("/some/path", ".print.a4.html/some/suffix");
+        assertEquals("/some/path", p.getResourcePath());
+        assertEquals("print.a4", p.getSelectorString());
+        assertEquals(2, p.getSelectors().length);
+        assertEquals("print", p.getSelectors()[0]);
+        assertEquals("a4", p.getSelectors()[1]);
+        assertEquals("html", p.getExtension());
+        assertEquals("/some/suffix", p.getSuffix());
+    }
+
+    @Test
+    public void testAllEmpty() {
+        RequestPathInfo p = createResourceUri("/", null);
+        assertEquals("/", p.getResourcePath());
+        assertNull("Selectors are null", p.getSelectorString());
+        assertEquals(0, p.getSelectors().length);
+        assertNull("Extension is null", p.getExtension());
+        assertNull("Suffix is null", p.getSuffix());
+    }
+
+    @Test
+    public void testPathOnly() {
+        RequestPathInfo p = createResourceUri("/some/path/here", "");
+        assertEquals("/some/path/here", p.getResourcePath());
+        assertNull("Selectors are null", p.getSelectorString());
+        assertEquals(0, p.getSelectors().length);
+        assertNull("Extension is null", p.getExtension());
+        assertNull("Suffix is null", p.getSuffix());
+    }
+
+    @Test
+    public void testPathWithExtensionOnly() {
+        RequestPathInfo p = createResourceUri("/some/path/here.html", "");
+        assertEquals("/some/path/here.html", p.getResourcePath());
+        assertNull("Selectors are null", p.getSelectorString());
+        assertEquals(0, p.getSelectors().length);
+        assertNull("Extension is null", p.getExtension());
+        assertNull("Suffix is null", p.getSuffix());
+    }
+
+    @Test
+    public void testPathAndExtensionOnly() {
+        RequestPathInfo p = createResourceUri("/some/path/here", ".html");
+        assertEquals("/some/path/here", p.getResourcePath());
+        assertNull("Selectors are null", p.getSelectorString());
+        assertEquals(0, p.getSelectors().length);
+        assertEquals("html", p.getExtension());
+        assertNull("Suffix is null", p.getSuffix());
+    }
+
+    @Test
+    public void testPathAndOneSelectorOnly() {
+        RequestPathInfo p = createResourceUri("/some/path/here", ".print.html");
+        assertEquals("/some/path/here", p.getResourcePath());
+        assertEquals("print", p.getSelectorString());
+        assertEquals(1, p.getSelectors().length);
+        assertEquals("print", p.getSelectors()[0]);
+        assertEquals("html", p.getExtension());
+        assertNull("Suffix is null", p.getSuffix());
+    }
+
+    @Test
+    public void testPathExtAndSuffix() {
+        RequestPathInfo p = createResourceUri("/some/path/here", ".html/something");
+        assertEquals("/some/path/here", p.getResourcePath());
+        assertNull("Selectors are null", p.getSelectorString());
+        assertEquals(0, p.getSelectors().length);
+        assertEquals("html", p.getExtension());
+        assertEquals("/something", p.getSuffix());
+    }
+
+    @Test
+    public void testSelectorsSplit() {
+        RequestPathInfo p = createResourceUri("/some/path", ".print.a4.html/some/suffix");
+        assertEquals("/some/path", p.getResourcePath());
+        assertEquals(2, p.getSelectors().length);
+        assertEquals("print", p.getSelectors()[0]);
+        assertEquals("a4", p.getSelectors()[1]);
+        assertEquals("html", p.getExtension());
+        assertEquals("/some/suffix", p.getSuffix());
+    }
+
+    @Test
+    public void testPartialResolutionB() {
+        RequestPathInfo p = createResourceUri("/some/path", ".print.a4.html/some/suffix");
+        assertEquals("/some/path", p.getResourcePath());
+        assertEquals("print.a4", p.getSelectorString());
+        assertEquals(2, p.getSelectors().length);
+        assertEquals("print", p.getSelectors()[0]);
+        assertEquals("a4", p.getSelectors()[1]);
+        assertEquals("html", p.getExtension());
+        assertEquals("/some/suffix", p.getSuffix());
+    }
+
+    @Test
+    public void testPartialResolutionC() {
+        RequestPathInfo p = createResourceUri("/some/path.print", ".a4.html/some/suffix");
+        assertEquals("/some/path.print", p.getResourcePath());
+        assertEquals("a4", p.getSelectorString());
+        assertEquals(1, p.getSelectors().length);
+        assertEquals("a4", p.getSelectors()[0]);
+        assertEquals("html", p.getExtension());
+        assertEquals("/some/suffix", p.getSuffix());
+    }
+
+    @Test
+    public void testPartialResolutionD() {
+        RequestPathInfo p = createResourceUri("/some/path.print.a4", ".html/some/suffix");
+        assertEquals("/some/path.print.a4", p.getResourcePath());
+        assertNull("Selectors are null", p.getSelectorString());
+        assertEquals(0, p.getSelectors().length);
+        assertEquals("html", p.getExtension());
+        assertEquals("/some/suffix", p.getSuffix());
+    }
+
+    @Test
+    public void testDotsAroundSuffix() {
+        RequestPathInfo p = createResourceUri("/libs/foo/content/something/formitems", ".json/image/vnd/xnd/knd.xml");
+        assertEquals("/libs/foo/content/something/formitems", p.getResourcePath());
+        assertEquals("json", p.getExtension());
+        assertNull("Selectors are null", p.getSelectorString());
+        assertEquals("/image/vnd/xnd/knd.xml", p.getSuffix());
+    }
+
+    @Test
+    public void testJIRA_250_a() {
+        RequestPathInfo p = createResourceUri("/bunkai", ".1.json");
+        assertEquals("/bunkai", p.getResourcePath());
+        assertEquals("json", p.getExtension());
+        assertEquals("1", p.getSelectorString());
+    }
+
+    @Test
+    public void testJIRA_250_b() {
+        RequestPathInfo p = createResourceUri("/", ".1.json");
+        assertEquals("/", p.getResourcePath());
+        assertEquals("json", p.getExtension());
+        assertNull("Suffix is null", p.getSuffix());
+        assertEquals("Selector string must not be null", "1",
+                p.getSelectorString());
+    }
+
+    @Test
+    public void testJIRA_250_c() {
+        RequestPathInfo p = createResourceUri("/", ".1.json/my/suffix");
+        assertEquals("/", p.getResourcePath());
+        assertEquals("json", p.getExtension());
+        assertEquals("/my/suffix", p.getSuffix());
+        assertEquals("Selector string must not be null", "1",
+                p.getSelectorString());
+    }
+
+    @Test
+    public void testJIRA_250_d() {
+        RequestPathInfo p = createResourceUri("/", ".json");
+        assertEquals("/", p.getResourcePath());
+        assertEquals("json", p.getExtension());
+        assertNull("Suffix is null", p.getSuffix());
+        assertNull("Selectors are null", p.getSelectorString());
+    }
+
+}