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/22 17:36:07 UTC

[sling-org-apache-sling-api] branch master updated: SLING-9745 Introduce SlingUri (immutable) and SlingUriBuilder

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

ghenzler pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-api.git


The following commit(s) were added to refs/heads/master by this push:
     new 0aa6f2c  SLING-9745 Introduce SlingUri (immutable) and SlingUriBuilder
0aa6f2c is described below

commit 0aa6f2cd6dbc1181827ab231677edeeab7c1aa7d
Author: Georg Henzler <gh...@users.noreply.github.com>
AuthorDate: Tue Sep 22 19:36:01 2020 +0200

    SLING-9745 Introduce SlingUri (immutable) and SlingUriBuilder
    
    SLING-9745 Introduce SlingUri (immutable) and SlingUriBuilder
    
    General purpose class to represent a SlingUri (like e.g. a link in an html <a> tag). Similar to JDK URI but supports Sling URI parts like selectors and suffix
---
 .../java/org/apache/sling/api/uri/SlingUri.java    |  235 +++++
 .../org/apache/sling/api/uri/SlingUriBuilder.java  | 1053 ++++++++++++++++++++
 .../org/apache/sling/api/uri/package-info.java     |   23 +
 .../apache/sling/api/uri/SlingUriBuilderTest.java  |  167 ++++
 .../uri/SlingUriBuilderWithAdjustMethodTest.java   |  313 ++++++
 .../sling/api/uri/SlingUriInvalidUrisTest.java     |   78 ++
 .../apache/sling/api/uri/SlingUriRebaseTest.java   |  231 +++++
 .../org/apache/sling/api/uri/SlingUriTest.java     |  443 ++++++++
 ...UriToSlingRequestPathInfoCompatibilityTest.java |  272 +++++
 9 files changed, 2815 insertions(+)

diff --git a/src/main/java/org/apache/sling/api/uri/SlingUri.java b/src/main/java/org/apache/sling/api/uri/SlingUri.java
new file mode 100644
index 0000000..2ad757e
--- /dev/null
+++ b/src/main/java/org/apache/sling/api/uri/SlingUri.java
@@ -0,0 +1,235 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.api.uri;
+
+import java.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;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.osgi.annotation.versioning.ProviderType;
+
+/**
+ * Represents an immutable URI in the same way as java.net.URI but is extended with Sling-specific URI parts (e.g. selectors). A SlingUri
+ * usually points to a resource but alternatively, can also contain an opaque URI like {@code mailto:} or {@code javascript:}. Use
+ * {@link SlingUri#adjust(Consumer)} or {@link SlingUriBuilder} to create new or modified Sling URIs.
+ * 
+ * @since 1.0.0 (Sling API Bundle 2.23.0)
+ */
+@ProviderType
+public interface SlingUri extends RequestPathInfo {
+
+    /**
+     * Returns the {@link URI}.
+     * 
+     * @return the URI
+     */
+    @NotNull
+    URI toUri();
+
+    /**
+     * Returns the URI as String.
+     * 
+     * @return the URI string
+     */
+    @NotNull
+    String toString();
+
+    /**
+     * Returns the scheme.
+     * 
+     * @return the scheme or null if not set
+     */
+    @Nullable
+    String getScheme();
+
+    /**
+     * Returns the user info.
+     * 
+     * @return the user info of the SlingUri or null if not set
+     */
+    @Nullable
+    String getUserInfo();
+
+    /**
+     * Returns the host.
+     * 
+     * @return returns the host of the SlingUri or null if not set
+     */
+    @Nullable
+    String getHost();
+
+    /**
+     * Returns the port.
+     * 
+     * @return returns the port of the SlingUri or -1 if not set
+     */
+    int getPort();
+
+    /**
+     * Returns the resource path.
+     * 
+     * @return returns the resource path or null if the URI does not contain a path.
+     */
+    @Override
+    @Nullable
+    String getResourcePath();
+
+    /**
+     * Returns the selector string.
+     * 
+     * @return returns the selector string or null if the URI does not contain selector(s)
+     */
+    @Override
+    @Nullable
+    String getSelectorString();
+
+    /**
+     * Returns the selectors array.
+     * 
+     * @return the selectors array (empty if the URI does not contain selector(s), never null)
+     */
+    @Override
+    @NotNull
+    String[] getSelectors();
+
+    /**
+     * Returns the extension.
+     * 
+     * @return the extension or null if the URI does not contain an extension
+     */
+    @Override
+    @Nullable
+    String getExtension();
+
+    /**
+     * Returns the path parameters.
+     * 
+     * @return the path parameters or an empty Map if the URI does not contain any
+     */
+    @NotNull
+    Map<String, String> getPathParameters();
+
+    /**
+     * Returns the suffix part of the URI
+     * 
+     * @return the suffix string or null if the URI does not contain a suffix
+     */
+    @Override
+    @Nullable
+    String getSuffix();
+
+    /**
+     * Returns the joint path of resource path, selectors, extension and suffix.
+     * 
+     * @return the path or null if no path is set
+     */
+    @Nullable
+    String getPath();
+
+    /**
+     * Returns the query.
+     * 
+     * @return the query part of the URI or null if the URI does not contain a query
+     */
+    @Nullable
+    String getQuery();
+
+    /**
+     * Returns the fragment.
+     * 
+     * @return the fragment or null if the URI does not contain a fragment
+     */
+    @Nullable
+    String getFragment();
+
+    /**
+     * Returns the scheme-specific part of the URI, compare with Javadoc of {@link URI}.
+     * 
+     * @return scheme specific part of the URI
+     */
+    @Nullable
+    String getSchemeSpecificPart();
+
+    /**
+     * Returns the corresponding suffix resource or null if
+     * <ul>
+     * <li>no resource resolver is available (depends on the create method used in SlingUriBuilder)</li>
+     * <li>the URI does not contain a suffix</li>
+     * <li>if the suffix resource could not be found</li>
+     * </ul>
+     * 
+     * @return the suffix resource if available or null
+     */
+    @Override
+    @Nullable
+    Resource getSuffixResource();
+
+    /**
+     * Returns true the URI is either a relative or absolute path (this is the case if scheme and host is empty and the URI path is set)
+     * 
+     * @return returns true for path URIs
+     */
+    boolean isPath();
+
+    /**
+     * Returns true if the URI has an absolute path starting with a slash ('/').
+     * 
+     * @return true if the URI is an absolute path
+     */
+    boolean isAbsolutePath();
+
+    /**
+     * Returns true if the URI is a relative path (no scheme and path does not start with '/').
+     * 
+     * @return true if URI is a relative path
+     */
+    boolean isRelativePath();
+
+    /**
+     * Returns true the URI is an absolute URI.
+     * 
+     * @return true if the URI is an absolute URI containing a scheme.
+     */
+    boolean isAbsolute();
+
+    /**
+     * Returns true for opaque URIs like e.g. mailto:jon@example.com.
+     * 
+     * @return true if the URI is an opaque URI
+     */
+    boolean isOpaque();
+
+    /**
+     * Shortcut to adjust Sling URIs, e.g. {@code slingUri = slingUri.adjust(b -> b.setExtension("html")); }.
+     * 
+     * @param builderConsumer the consumer (e.g. {@code b -> b.setExtension("html")})
+     * @return the adjusted SlingUri (new instance)
+     */
+    @NotNull
+    default SlingUri adjust(@NotNull Consumer<SlingUriBuilder> builderConsumer) {
+        SlingUriBuilder builder = SlingUriBuilder.createFrom(this);
+        builderConsumer.accept(builder);
+        return builder.build();
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/api/uri/SlingUriBuilder.java b/src/main/java/org/apache/sling/api/uri/SlingUriBuilder.java
new file mode 100644
index 0000000..29ef397
--- /dev/null
+++ b/src/main/java/org/apache/sling/api/uri/SlingUriBuilder.java
@@ -0,0 +1,1053 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.api.uri;
+
+import java.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.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.request.RequestPathInfo;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.osgi.annotation.versioning.ProviderType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Builder for SlingUris.
+ * <p>
+ * Example:
+ * 
+ * <pre>
+ * SlingUri testUri = SlingUriBuilder.create()
+ *         .setResourcePath("/test/to/path")
+ *         .setSelectors(new String[] { "sel1", "sel2" })
+ *         .setExtension("html")
+ *         .setSuffix("/suffix/path")
+ *         .setQuery("par1=val1&amp;par2=val2")
+ *         .build();
+ * </pre>
+ * <p>
+ * 
+ * @since 1.0.0 (Sling API Bundle 2.23.0)
+ */
+@ProviderType
+public class SlingUriBuilder {
+    private static final Logger LOG = LoggerFactory.getLogger(SlingUriBuilder.class);
+
+    private static final String HTTPS_SCHEME = "https";
+    private static final int HTTPS_DEFAULT_PORT = 443;
+    private static final String HTTP_SCHEME = "http";
+    private static final int HTTP_DEFAULT_PORT = 80;
+
+    static final char CHAR_HASH = '#';
+    static final char CHAR_QM = '?';
+    static final char CHAR_AT = '@';
+    static final char CHAR_SEMICOLON = ';';
+    static final char CHAR_EQUALS = '=';
+    static final char CHAR_SINGLEQUOTE = '\'';
+    static final String CHAR_COLON = ":";
+    static final String CHAR_DOT = ".";
+    static final String CHAR_SLASH = "/";
+    static final String SELECTOR_DOT_REGEX = "\\.(?!\\.?/)"; // (?!\\.?/) to avoid matching ./ and ../
+    static final String PATH_PARAMETERS_REGEX = ";([a-zA-z0-9]+)=(?:\\'([^']*)\\'|([^/]+))";
+
+    /**
+     * Creates a builder without any URI parameters set.
+     * 
+     * @return a SlingUriBuilder
+     */
+    @NotNull
+    public static SlingUriBuilder create() {
+        return new SlingUriBuilder();
+    }
+
+    /**
+     * Creates a builder from another SlingUri (clone and modify use case).
+     * 
+     * @param slingUri the Sling URI to clone
+     * @return a SlingUriBuilder
+     */
+    @NotNull
+    public static SlingUriBuilder createFrom(@NotNull SlingUri slingUri) {
+        return create()
+                .setScheme(slingUri.getScheme())
+                .setUserInfo(slingUri.getUserInfo())
+                .setHost(slingUri.getHost())
+                .setPort(slingUri.getPort())
+                .setResourcePath(slingUri.getResourcePath())
+                .setPathParameters(slingUri.getPathParameters())
+                .setSelectors(slingUri.getSelectors())
+                .setExtension(slingUri.getExtension())
+                .setSuffix(slingUri.getSuffix())
+                .setQuery(slingUri.getQuery())
+                .setFragment(slingUri.getFragment())
+                .setSchemeSpecificPart(slingUri.isOpaque() ? slingUri.getSchemeSpecificPart() : null)
+                .setResourceResolver(slingUri instanceof ImmutableSlingUri
+                        ? ((ImmutableSlingUri) slingUri).getBuilder().resourceResolver
+                        : null);
+    }
+
+    /**
+     * Creates a builder from a resource (only taking the resource path into account).
+     * 
+     * @param resource the resource to take the resource path from
+     * @return a SlingUriBuilder
+     */
+    @NotNull
+    public static SlingUriBuilder createFrom(@NotNull Resource resource) {
+        return create()
+                .setResourcePath(resource.getPath())
+                .setResourceResolver(resource.getResourceResolver());
+    }
+
+    /**
+     * Creates a builder from a RequestPathInfo instance .
+     * 
+     * @param requestPathInfo the request path info to take resource path, selectors, extension and suffix from.
+     * @return a SlingUriBuilder
+     */
+    @NotNull
+    public static SlingUriBuilder createFrom(@NotNull RequestPathInfo requestPathInfo) {
+        Resource suffixResource = requestPathInfo.getSuffixResource();
+        return create()
+                .setResourceResolver(suffixResource != null ? suffixResource.getResourceResolver() : null)
+                .setResourcePath(requestPathInfo.getResourcePath())
+                .setSelectors(requestPathInfo.getSelectors())
+                .setExtension(requestPathInfo.getExtension())
+                .setSuffix(requestPathInfo.getSuffix());
+    }
+
+    /**
+     * Creates a builder from a request.
+     * 
+     * @param request request to take the URI information from
+     * @return a SlingUriBuilder
+     */
+    @NotNull
+    public static SlingUriBuilder createFrom(@NotNull SlingHttpServletRequest request) {
+        return createFrom(request.getRequestPathInfo())
+                .setResourceResolver(request.getResourceResolver())
+                .setScheme(request.getScheme())
+                .setHost(request.getServerName())
+                .setPort(request.getServerPort())
+                .setQuery(request.getQueryString());
+    }
+
+    /**
+     * Creates a builder from an arbitrary URI.
+     * 
+     * @param uri the uri to transform to a SlingUri
+     * @param resourceResolver a resource resolver is needed to decide up to what part the path is the resource path (that decision is only
+     *        possible by checking against the underlying repository). If null is passed in, the shortest viable resource path is used.
+     * @return a SlingUriBuilder
+     */
+    @NotNull
+    public static SlingUriBuilder createFrom(@NotNull URI uri, @Nullable ResourceResolver resourceResolver) {
+        String path = uri.getPath();
+        boolean pathExists = isNotBlank(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 SlingUriBuilder
+     */
+    @NotNull
+    public static SlingUriBuilder parse(@NotNull String uriStr, @Nullable ResourceResolver resourceResolver) {
+        URI uri;
+        try {
+            uri = new URI(uriStr);
+            return createFrom(uri, resourceResolver);
+        } catch (URISyntaxException e) {
+            LOG.debug("Invalid URI {}: {}", uriStr, e.getMessage(), e);
+            // best effort
+            String[] invalidUriParts = uriStr.split(CHAR_COLON, 2);
+            if (invalidUriParts.length == 1) {
+                return create().setSchemeSpecificPart(invalidUriParts[0]);
+            } else {
+                return create()
+                        .setScheme(invalidUriParts[0])
+                        .setSchemeSpecificPart(invalidUriParts[1]);
+            }
+        }
+    }
+
+    // simple helper to avoid StringUtils dependency
+    private static boolean isBlank(final CharSequence cs) {
+        return cs == null || cs.chars().allMatch(Character::isWhitespace);
+    }
+
+    private static boolean isNotBlank(final CharSequence cs) {
+        return !isBlank(cs);
+    }
+
+    private String scheme = null;
+
+    private String userInfo = null;
+    private String host = null;
+    private int port = -1;
+
+    private String resourcePath = null;
+    private final List<String> selectors = new LinkedList<>();
+    private String extension = null;
+    private final Map<String, String> pathParameters = new LinkedHashMap<>();
+    private String suffix = null;
+    private String schemeSpecificPart = null;
+    private String query = null;
+    private String fragment = null;
+
+    // needed for getSuffixResource() from interface RequestPathInfo and rebaseResourcePath()
+    private ResourceResolver resourceResolver = null;
+
+    // to ensure a builder is used only once (as the ImmutableSlingUri being created in build() is sharing its state)
+    private boolean isBuilt = false;
+
+    private SlingUriBuilder() {
+    }
+
+    /**
+     * Set the user info of the URI.
+     * 
+     * @param userInfo the user info
+     * @return the builder for method chaining
+     */
+    @NotNull
+    public SlingUriBuilder setUserInfo(@Nullable String userInfo) {
+        if (schemeSpecificPart != null) {
+            return this;
+        }
+        this.userInfo = userInfo;
+        return this;
+    }
+
+    /**
+     * Set the host of the URI.
+     * 
+     * @param host the host
+     * @return the builder for method chaining
+     */
+    @NotNull
+    public SlingUriBuilder setHost(@Nullable String host) {
+        if (schemeSpecificPart != null) {
+            return this;
+        }
+        this.host = host;
+        return this;
+    }
+
+    /**
+     * Set the port of the URI.
+     * 
+     * @param port the port
+     * @return the builder for method chaining
+     */
+    @NotNull
+    public SlingUriBuilder setPort(int port) {
+        if (schemeSpecificPart != null) {
+            return this;
+        }
+        this.port = port;
+        return this;
+    }
+
+    /**
+     * Set the path of the URI that contains a resource path and optionally path parameters, selectors, an extension and a suffix. To remove
+     * an existing path set path to {@code null}.
+     * 
+     * @param path the path
+     * @return the builder for method chaining
+     */
+    @NotNull
+    public SlingUriBuilder setPath(@Nullable String path) {
+        if (schemeSpecificPart != null) {
+            return this;
+        }
+
+        // adds path parameters to this.pathParameters and returns path without those
+        path = extractPathParameters(path);
+
+        // split in resource path, selectors, extension and suffix
+        Matcher dotMatcher;
+        if (path != null && path.startsWith(SlingUriBuilder.CHAR_SLASH) && resourceResolver != null) {
+            setResourcePath(path);
+            rebaseResourcePath();
+        } else if (path != null && (dotMatcher = Pattern.compile(SELECTOR_DOT_REGEX).matcher(path)).find()) {
+            int firstDotPosition = dotMatcher.start();
+            setPathWithDefinedResourcePosition(path, firstDotPosition);
+        } else {
+            setSelectors(new String[] {});
+            setSuffix(null);
+            setExtension(null);
+            setResourcePath(path);
+        }
+
+        return this;
+    }
+
+    /**
+     * Will rebase the uri based on the underlying resource structure. A resource resolver is necessary for this operation, hence
+     * setResourceResolver() needs to be called before balanceResourcePath() or a create method that implicitly sets this has to be used.
+     * 
+     * @return the builder for method chaining
+     * @throws IllegalStateException
+     *             if no resource resolver is available
+     */
+    @NotNull
+    public SlingUriBuilder rebaseResourcePath() {
+        if (schemeSpecificPart != null || resourcePath == null) {
+            return this;
+        }
+        if (resourceResolver == null) {
+            throw new IllegalStateException("setResourceResolver() needs to be called before balanceResourcePath()");
+        }
+
+        String path = assemblePath(false);
+        if (path == null) {
+            return this; // nothing to rebase
+        }
+        ResourcePathIterator it = new ResourcePathIterator(path);
+        String availableResourcePath = null;
+        while (it.hasNext()) {
+            availableResourcePath = it.next();
+            if (resourceResolver.getResource(availableResourcePath) != null) {
+                break;
+            }
+        }
+        if (availableResourcePath == null) {
+            return this; // nothing to rebase
+        }
+
+        selectors.clear();
+        extension = null;
+        suffix = null;
+        if (availableResourcePath.length() == path.length()) {
+            resourcePath = availableResourcePath;
+        } else {
+            setPathWithDefinedResourcePosition(path, availableResourcePath.length());
+        }
+        return this;
+    }
+
+    /**
+     * Set the resource path of the URI.
+     * 
+     * @param resourcePath the resource path
+     * @return the builder for method chaining
+     */
+    @NotNull
+    public SlingUriBuilder setResourcePath(@Nullable String resourcePath) {
+        if (schemeSpecificPart != null) {
+            return this;
+        }
+        this.resourcePath = resourcePath;
+        return this;
+    }
+
+    /**
+     * Set the selectors of the URI.
+     * 
+     * @param selectors the selectors
+     * @return the builder for method chaining
+     */
+    @NotNull
+    public SlingUriBuilder setSelectors(@NotNull String[] selectors) {
+        if (schemeSpecificPart != null || resourcePath == null) {
+            return this;
+        }
+        this.selectors.clear();
+        Arrays.stream(selectors).forEach(this.selectors::add);
+        return this;
+    }
+
+    /**
+     * Add a selector to the URI.
+     * 
+     * @param selector the selector to add
+     * @return the builder for method chaining
+     */
+    @NotNull
+    public SlingUriBuilder addSelector(@NotNull String selector) {
+        if (schemeSpecificPart != null || resourcePath == null) {
+            return this;
+        }
+        this.selectors.add(selector);
+        return this;
+    }
+
+    /**
+     * Set the extension of the URI.
+     * 
+     * @param extension the extension
+     * @return the builder for method chaining
+     */
+    @NotNull
+    public SlingUriBuilder setExtension(@Nullable String extension) {
+        if (schemeSpecificPart != null || resourcePath == null) {
+            return this;
+        }
+        this.extension = extension;
+        return this;
+    }
+
+    /**
+     * Set a path parameter to the URI.
+     * 
+     * @param key the path parameter key
+     * @param value the path parameter value
+     * @return the builder for method chaining
+     */
+    @NotNull
+    public SlingUriBuilder setPathParameter(@NotNull String key, @NotNull String value) {
+        if (schemeSpecificPart != null || resourcePath == null) {
+            return this;
+        }
+        this.pathParameters.put(key, value);
+        return this;
+    }
+
+    /**
+     * Replaces all path parameters in the URI.
+     * 
+     * @param pathParameters the path parameters
+     * @return the builder for method chaining
+     */
+    @NotNull
+    public SlingUriBuilder setPathParameters(@NotNull Map<String, String> pathParameters) {
+        this.pathParameters.clear();
+        this.pathParameters.putAll(pathParameters);
+        return this;
+    }
+
+    /**
+     * Set the suffix of the URI.
+     * 
+     * @param suffix the suffix
+     * @return the builder for method chaining
+     */
+    @NotNull
+    public SlingUriBuilder setSuffix(@Nullable String suffix) {
+        if (schemeSpecificPart != null || resourcePath == null) {
+            return this;
+        }
+        if (suffix != null && !suffix.startsWith("/")) {
+            throw new IllegalArgumentException("Suffix needs to start with slash");
+        }
+        this.suffix = suffix;
+        return this;
+    }
+
+    /**
+     * Set the query of the URI.
+     * 
+     * @param query the query
+     * @return the builder for method chaining
+     */
+    @NotNull
+    public SlingUriBuilder setQuery(@Nullable String query) {
+        if (schemeSpecificPart != null) {
+            return this;
+        }
+        this.query = query;
+        return this;
+    }
+
+    /**
+     * Set the fragment of the URI.
+     * 
+     * @param fragment the fragment
+     * @return the builder for method chaining
+     */
+    @NotNull
+    public SlingUriBuilder setFragment(@Nullable String fragment) {
+        if (schemeSpecificPart != null) {
+            return this;
+        }
+        this.fragment = fragment;
+        return this;
+    }
+
+    /**
+     * Set the scheme of the URI.
+     * 
+     * @param scheme the scheme
+     * @return the builder for method chaining
+     */
+    @NotNull
+    public SlingUriBuilder setScheme(@Nullable String scheme) {
+        this.scheme = scheme;
+        return this;
+    }
+
+    /**
+     * Set the scheme specific part of the URI. Use this for e.g. mail:jon@example.com URIs.
+     * 
+     * @param schemeSpecificPart the scheme specific part
+     * @return the builder for method chaining
+     */
+    @NotNull
+    public SlingUriBuilder setSchemeSpecificPart(@Nullable String schemeSpecificPart) {
+        this.schemeSpecificPart = schemeSpecificPart;
+        return this;
+    }
+
+    /**
+     * Will remove scheme and authority (that is user info, host and port).
+     * 
+     * @return the builder for method chaining
+     */
+    @NotNull
+    public SlingUriBuilder removeSchemeAndAuthority() {
+        setScheme(null);
+        setUserInfo(null);
+        setHost(null);
+        setPort(-1);
+        return this;
+    }
+
+    /**
+     * Will take over scheme and authority (user info, host and port) from provided slingUri.
+     * 
+     * @param slingUri the Sling URI
+     * @return the builder for method chaining
+     */
+    @NotNull
+    public SlingUriBuilder useSchemeAndAuthority(@NotNull SlingUri slingUri) {
+        setScheme(slingUri.getScheme());
+        setUserInfo(slingUri.getUserInfo());
+        setHost(slingUri.getHost());
+        setPort(slingUri.getPort());
+        return this;
+    }
+
+    /**
+     * Will take over scheme and authority (user info, host and port) from provided URI.
+     * 
+     * @param uri the URI
+     * @return the builder for method chaining
+     */
+    @NotNull
+    public SlingUriBuilder useSchemeAndAuthority(@NotNull URI uri) {
+        useSchemeAndAuthority(createFrom(uri, resourceResolver).build());
+        return this;
+    }
+
+    /**
+     * Sets the resource resolver (required for {@link RequestPathInfo#getSuffixResource()}).
+     * 
+     * @param resourceResolver the resource resolver
+     * @return the builder for method chaining
+     */
+    @NotNull
+    public SlingUriBuilder setResourceResolver(ResourceResolver resourceResolver) {
+        this.resourceResolver = resourceResolver;
+        return this;
+    }
+
+    /** Builds the immutable SlingUri from this builder.
+     * 
+     * @return the builder for method chaining */
+    @NotNull
+    public SlingUri build() {
+        if (isBuilt) {
+            throw new IllegalStateException("SlingUriBuilder.build() may only be called once per builder instance");
+        }
+        isBuilt = true;
+        return new ImmutableSlingUri();
+    }
+
+    /**
+     * Builds the corresponding string URI for this builder.
+     * 
+     * @return string representation of builder
+     */
+    public String toString() {
+        return toStringInternal(true, true);
+    }
+
+
+    /**
+     * Returns true the URI is either a relative or absolute path (this is the case if scheme and host is empty and the URI path is set)
+     * 
+     * @return returns true for path URIs
+     */
+    public boolean isPath() {
+        return isBlank(scheme)
+                && isBlank(host)
+                && isNotBlank(resourcePath);
+    }
+
+    /**
+     * Returns true if the URI has an absolute path starting with a slash ('/').
+     * 
+     * @return true if the URI is an absolute path
+     */
+    public boolean isAbsolutePath() {
+        return isPath() && resourcePath.startsWith(SlingUriBuilder.CHAR_SLASH);
+    }
+
+    /**
+     * Returns true if the URI is a relative path (no scheme and path does not start with '/').
+     * 
+     * @return true if URI is a relative path
+     */
+    public boolean isRelativePath() {
+        return isPath() && !resourcePath.startsWith(SlingUriBuilder.CHAR_SLASH);
+    }
+
+    /**
+     * Returns true the URI is an absolute URI.
+     * 
+     * @return true if the URI is an absolute URI containing a scheme.
+     */
+    public boolean isAbsolute() {
+        return scheme != null;
+    }
+
+    /**
+     * Returns true for opaque URIs like e.g. mailto:jon@example.com.
+     * 
+     * @return true if the URI is an opaque URI
+     */
+    public boolean isOpaque() {
+        return scheme != null && schemeSpecificPart != null;
+    }
+
+    private String toStringInternal(boolean includeScheme, boolean includeFragment) {
+        StringBuilder requestUri = new StringBuilder();
+
+        if (isAbsolute()) {
+            if (includeScheme) {
+                requestUri.append(scheme + CHAR_COLON);
+            }
+            if (schemeSpecificPart == null) {
+                requestUri.append(CHAR_SLASH + CHAR_SLASH);
+                if (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);
+                    requestUri.append(port);
+                }
+            }
+        }
+        if (schemeSpecificPart != null) {
+            requestUri.append(schemeSpecificPart);
+        }
+        if (resourcePath != null) {
+            requestUri.append(assemblePath(true));
+        }
+        if (query != null) {
+            requestUri.append(CHAR_QM + query);
+        }
+        if (includeFragment && fragment != null) {
+            requestUri.append(CHAR_HASH + fragment);
+        }
+        return requestUri.toString();
+    }
+
+    private void setPathWithDefinedResourcePosition(String path, int firstDotPositionAfterResourcePath) {
+        setResourcePath(path.substring(0, firstDotPositionAfterResourcePath));
+        int firstSlashAfterFirstDotPosition = path.indexOf(CHAR_SLASH, firstDotPositionAfterResourcePath);
+        String pathWithoutSuffix = firstSlashAfterFirstDotPosition > -1
+                ? path.substring(firstDotPositionAfterResourcePath + 1, firstSlashAfterFirstDotPosition)
+                : path.substring(firstDotPositionAfterResourcePath + 1);
+        String[] pathBits = pathWithoutSuffix.split(SELECTOR_DOT_REGEX);
+        if (pathBits.length > 1) {
+            setSelectors(Arrays.copyOfRange(pathBits, 0, pathBits.length - 1));
+        }
+        setExtension(pathBits.length > 0 && pathBits[pathBits.length - 1].length() > 0 ? pathBits[pathBits.length - 1] : null);
+        setSuffix(firstSlashAfterFirstDotPosition > -1 ? path.substring(firstSlashAfterFirstDotPosition) : null);
+    }
+
+    private String extractPathParameters(String path) {
+        // we rebuild the parameters from scratch as given in path (if path is set to null we also reset)
+        pathParameters.clear();
+        if (path != null) {
+            Pattern pathParameterRegex = Pattern.compile(PATH_PARAMETERS_REGEX);
+
+            StringBuffer resultString = null;
+            Matcher regexMatcher = pathParameterRegex.matcher(path);
+            while (regexMatcher.find()) {
+                if (resultString == null) {
+                    resultString = new StringBuffer();
+                }
+                regexMatcher.appendReplacement(resultString, "");
+                String key = regexMatcher.group(1);
+                String value = isNotBlank(regexMatcher.group(2)) ? regexMatcher.group(2) : regexMatcher.group(3);
+                pathParameters.put(key, value);
+            }
+            if (resultString != null) {
+                regexMatcher.appendTail(resultString);
+                path = resultString.toString();
+            }
+        }
+        return path;
+    }
+
+    private String assemblePath(boolean includePathParamters) {
+        if (resourcePath == null) {
+            return null;
+        }
+
+        StringBuilder pathBuilder = new StringBuilder();
+        pathBuilder.append(resourcePath);
+        if (includePathParamters && !pathParameters.isEmpty()) {
+            for (Map.Entry<String, String> pathParameter : pathParameters.entrySet()) {
+                pathBuilder.append(CHAR_SEMICOLON + pathParameter.getKey() + CHAR_EQUALS +
+                        CHAR_SINGLEQUOTE + pathParameter.getValue() + CHAR_SINGLEQUOTE);
+            }
+        }
+
+        boolean dotAdded = false;
+        if (!selectors.isEmpty()) {
+            pathBuilder.append(CHAR_DOT + String.join(CHAR_DOT, selectors));
+            dotAdded = true;
+        }
+        if (isNotBlank(extension)) {
+            pathBuilder.append(CHAR_DOT + extension);
+            dotAdded = true;
+        }
+
+        if (isNotBlank(suffix)) {
+            if (!dotAdded) {
+                pathBuilder.append(CHAR_DOT);
+            }
+            pathBuilder.append(suffix);
+        }
+        return pathBuilder.toString();
+    }
+
+
+    // read-only view on the builder data (to avoid another copy of the data into a new object)
+    private class ImmutableSlingUri implements SlingUri {
+
+        @Override
+        public String getResourcePath() {
+            return 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() {
+            if (isOpaque()) {
+                return schemeSpecificPart;
+            } else {
+                return toStringInternal(false, false);
+            }
+        }
+
+        @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 (isNotBlank(suffix) && resourceResolver != null) {
+                return resourceResolver.resolve(suffix);
+            } else {
+                return null;
+            }
+        }
+
+        @Override
+        public String getUserInfo() {
+            return userInfo;
+        }
+
+        @Override
+        public boolean isOpaque() {
+            return getBuilder().isOpaque();
+        }
+
+        @Override
+        public boolean isPath() {
+            return getBuilder().isPath();
+        }
+
+        @Override
+        public boolean isAbsolutePath() {
+            return getBuilder().isAbsolutePath();
+        }
+
+        @Override
+        public boolean isRelativePath() {
+            return getBuilder().isRelativePath();
+        }
+
+        @Override
+        public boolean isAbsolute() {
+            return getBuilder().isAbsolute();
+        }
+
+        @Override
+        public String toString() {
+            return toStringInternal(true, true);
+        }
+
+        @Override
+        public URI toUri() {
+            String uriString = toString();
+            try {
+                return new URI(uriString);
+            } catch (URISyntaxException e) {
+                throw new IllegalStateException("Invalid Sling URI: " + uriString, e);
+            }
+        }
+
+        private SlingUriBuilder getBuilder() {
+            return SlingUriBuilder.this;
+        }
+
+        // generated hashCode() and equals()
+        @Override
+        public int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + ((extension == null) ? 0 : extension.hashCode());
+            result = prime * result + ((fragment == null) ? 0 : fragment.hashCode());
+            result = prime * result + ((host == null) ? 0 : host.hashCode());
+            result = prime * result + pathParameters.hashCode();
+            result = prime * result + port;
+            result = prime * result + ((query == null) ? 0 : query.hashCode());
+            result = prime * result + ((resourcePath == null) ? 0 : resourcePath.hashCode());
+            result = prime * result + ((scheme == null) ? 0 : scheme.hashCode());
+            result = prime * result + ((schemeSpecificPart == null) ? 0 : schemeSpecificPart.hashCode());
+            result = prime * result + selectors.hashCode();
+            result = prime * result + ((suffix == null) ? 0 : suffix.hashCode());
+            result = prime * result + ((userInfo == null) ? 0 : userInfo.hashCode());
+            return result;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj)
+                return true;
+            if (obj == null)
+                return false;
+            if (getClass() != obj.getClass())
+                return false;
+            ImmutableSlingUri other = (ImmutableSlingUri) obj;
+            if (extension == null) {
+                if (other.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;
+        }
+
+        @Override
+        public void remove() {
+            throw new UnsupportedOperationException("remove");
+        }
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/api/uri/package-info.java b/src/main/java/org/apache/sling/api/uri/package-info.java
new file mode 100644
index 0000000..c6b4053
--- /dev/null
+++ b/src/main/java/org/apache/sling/api/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.uri;
+
+import org.osgi.annotation.versioning.Version;
+
diff --git a/src/test/java/org/apache/sling/api/uri/SlingUriBuilderTest.java b/src/test/java/org/apache/sling/api/uri/SlingUriBuilderTest.java
new file mode 100644
index 0000000..0f33031
--- /dev/null
+++ b/src/test/java/org/apache/sling/api/uri/SlingUriBuilderTest.java
@@ -0,0 +1,167 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.api.uri;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+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 SlingUriBuilderTest {
+
+    @Mock
+    SlingHttpServletRequest request;
+
+    @Mock
+    RequestPathInfo requestPathInfo;
+
+    @Mock
+    Resource resource;
+
+    @Before
+    public void before() {
+        when(request.getRequestPathInfo()).thenReturn(requestPathInfo);
+    }
+
+    @Test
+    public void testBasicUsage() {
+
+        SlingUri testUri = SlingUriBuilder.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 SlingUriTest extensively test the builder's parse method by using it for constructing
+    // all types of SlingUris
+    @Test
+    public void testParse() {
+
+        String testUriStr = "https://example.com/test/to/path.sel1.sel2.html";
+        SlingUri testUri = SlingUriBuilder.parse(testUriStr, null).build();
+        assertEquals(testUriStr, testUri.toString());
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testSetInvalidSuffix() {
+        SlingUriBuilder.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");
+
+        SlingUri testUri = SlingUriBuilder.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");
+        SlingUri testUri = SlingUriBuilder.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");
+        SlingUri testUri = SlingUriBuilder.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");
+
+        SlingUri testUri = SlingUriBuilder.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";
+        SlingUri testUri = SlingUriBuilder.parse(testPath, null)
+                .useSchemeAndAuthority(testUriToUseSchemeAndAuthorityFrom)
+                .build();
+        assertEquals("https://example.com:8080/path/to/page.html", testUri.toString());
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testBuilderMayOnlyBeUsedToBuildAnUri() {
+        SlingUriBuilder builder = SlingUriBuilder.parse("/path/to/page.html", null);
+        SlingUri slingUri = builder.build();
+        assertNotNull(slingUri);
+        // calling build twice is not allowed
+        builder.build();
+    }
+
+    @Test
+    public void testEmpty() {
+        SlingUri testUriEmpty = SlingUriBuilder.create().build();
+        assertEquals("", testUriEmpty.toString());
+    }
+}
diff --git a/src/test/java/org/apache/sling/api/uri/SlingUriBuilderWithAdjustMethodTest.java b/src/test/java/org/apache/sling/api/uri/SlingUriBuilderWithAdjustMethodTest.java
new file mode 100644
index 0000000..00afcd0
--- /dev/null
+++ b/src/test/java/org/apache/sling/api/uri/SlingUriBuilderWithAdjustMethodTest.java
@@ -0,0 +1,313 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.api.uri;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+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 SlingUriBuilderWithAdjustMethodTest {
+
+    @Test
+    public void testAdjustAddSelectorFullUrl() {
+
+        testAdjustUri(
+                "http://host.com/test/to/path.html",
+                slingUriBuilder -> {
+                    slingUriBuilder.addSelector("test");
+                },
+                "http://host.com/test/to/path.test.html",
+                slingUri -> {
+                    assertEquals("test", slingUri.getSelectorString());
+                });
+    }
+
+    @Test
+    public void testAdjustAddSelectorAndSuffixPath() {
+
+        testAdjustUri(
+                "/test/to/path.html",
+                slingUriBuilder -> {
+                    slingUriBuilder.addSelector("test");
+                    slingUriBuilder.setSuffix("/suffix/path/to/file");
+                },
+                "/test/to/path.test.html/suffix/path/to/file",
+                slingUri -> {
+                    assertArrayEquals(new String[] { "test" }, slingUri.getSelectors());
+                    assertEquals("/suffix/path/to/file", slingUri.getSuffix());
+                });
+    }
+
+    @Test
+    public void testExtendSimplePathToFullUrl() {
+
+        testAdjustUri(
+                "/test/to/path.html",
+                slingUriBuilder -> {
+                    slingUriBuilder.setScheme("https");
+                    slingUriBuilder.setHost("example.com");
+                    slingUriBuilder.setSuffix("/suffix/path/to/file");
+                },
+                "https://example.com/test/to/path.html/suffix/path/to/file",
+                slingUri -> {
+                    assertEquals("https", slingUri.getScheme());
+                    assertEquals("example.com", slingUri.getHost());
+                    assertEquals("/suffix/path/to/file", slingUri.getSuffix());
+                });
+    }
+
+    @Test
+    public void testSetSelectorAndSuffixToRelativeUrl() {
+
+        testAdjustUri(
+                "../to/path.html",
+                slingUriBuilder -> {
+                    slingUriBuilder.addSelector("sel1");
+                    slingUriBuilder.setSuffix("/suffix/path/to/file");
+                },
+                "../to/path.sel1.html/suffix/path/to/file",
+                slingUri -> {
+                    assertEquals("../to/path", slingUri.getResourcePath());
+                    assertEquals("sel1", slingUri.getSelectorString());
+                    assertEquals("/suffix/path/to/file", slingUri.getSuffix());
+                });
+    }
+
+    @Test
+    public void testFullUrltoSimplePath() {
+
+        testAdjustUri(
+                "https://user:pw@example.com/test/to/path.html/suffix/path/to/file",
+                slingUriBuilder -> {
+                    slingUriBuilder.removeSchemeAndAuthority();
+                },
+                "/test/to/path.html/suffix/path/to/file",
+                slingUri -> {
+                    assertEquals(null, slingUri.getScheme());
+                    assertEquals(null, slingUri.getUserInfo());
+                    assertEquals(null, slingUri.getHost());
+                });
+    }
+
+    @Test
+    public void testAdjustPathInOpaqueUriWithoutEffect() {
+
+        testAdjustUri(
+                "mailto:jon.doe@example.com",
+                slingUriBuilder -> {
+                    slingUriBuilder.setUserInfo("user:pw");
+                    slingUriBuilder.setHost("example.com");
+                    slingUriBuilder.setPort(500);
+                    slingUriBuilder.setPath("/path/to/resource");
+                    slingUriBuilder.setResourcePath("/path/to/resource");
+                    slingUriBuilder.addSelector("test");
+                    slingUriBuilder.setExtension("html");
+                    slingUriBuilder.setSuffix("/suffix");
+                },
+                "mailto:jon.doe@example.com",
+                slingUri -> {
+                    assertNull(slingUri.getHost());
+                    assertNull(slingUri.getResourcePath());
+                    assertNull(slingUri.getSelectorString());
+                    assertNull(slingUri.getExtension());
+                    assertNull(slingUri.getSuffix());
+                });
+    }
+
+    @Test
+    public void testAdjustOpaqueToNormalUrl() {
+
+        testAdjustUri(
+                "mailto:jon.doe@example.com",
+                slingUriBuilder -> {
+                    slingUriBuilder.setSchemeSpecificPart(null);
+                    slingUriBuilder.setScheme("https");
+                    slingUriBuilder.setHost("example.com");
+                    slingUriBuilder.setPath("/path/to/resource.html");
+                },
+                "https://example.com/path/to/resource.html",
+                slingUri -> {
+                    assertEquals("//example.com/path/to/resource.html", slingUri.getSchemeSpecificPart());
+                });
+    }
+
+    @Test
+    public void testAdjustOpaqueUri() {
+
+        testAdjustUri(
+                "mailto:jon.doe@example.com",
+                slingUriBuilder -> {
+                    slingUriBuilder.setSchemeSpecificPart("mary.doe@example.com");
+                },
+                "mailto:mary.doe@example.com",
+                slingUri -> {
+                    assertEquals("mary.doe@example.com", slingUri.getSchemeSpecificPart());
+                    assertNull(slingUri.getResourcePath());
+                });
+    }
+
+    @Test
+    public void testAdjustSelectorsInFragmentOnlyUrlWithoutEffect() {
+
+        testAdjustUri(
+                "#fragment",
+                slingUriBuilder -> {
+                    slingUriBuilder.addSelector("test");
+                    slingUriBuilder.setSuffix("/suffix");
+                },
+                "#fragment",
+                slingUri -> {
+                    assertEquals(null, slingUri.getSelectorString());
+                    assertEquals(null, slingUri.getSuffix());
+                });
+    }
+
+    @Test
+    public void testAjustFtpUrl() {
+
+        testAdjustUri(
+                "sftp://user:pw@example.com:9090/some/path",
+                slingUriBuilder -> {
+                    slingUriBuilder.setPath("/some/other/path");
+                    slingUriBuilder.setPort(9091);
+                },
+                "sftp://user:pw@example.com:9091/some/other/path",
+                slingUri -> {
+                    assertEquals("/some/other/path", slingUri.getResourcePath());
+                    assertEquals(null, slingUri.getSelectorString());
+                    assertEquals(9091, slingUri.getPort());
+                });
+    }
+
+    @Test
+    public void testAdjustPathParameter() {
+
+        testAdjustUri(
+                "/test/to/path.sel1.html/suffix/path/to/file",
+                slingUriBuilder -> {
+                    slingUriBuilder.setPathParameter("v", "2.0");
+                },
+                "/test/to/path;v='2.0'.sel1.html/suffix/path/to/file",
+                slingUri -> {
+                    assertEquals("/test/to/path", slingUri.getResourcePath());
+                    assertEquals("sel1", slingUri.getSelectorString());
+                    assertEquals("html", slingUri.getExtension());
+                    assertEquals("/suffix/path/to/file", slingUri.getSuffix());
+                    assertEquals(1, slingUri.getPathParameters().size());
+                    assertEquals("2.0", slingUri.getPathParameters().get("v"));
+                });
+    }
+
+    @Test
+    public void testAdjustRemovePath() {
+
+        testAdjustUri(
+                "http://example.com/test/to/path;key='val'.sel1.html/suffix/path/to/file?queryPar=val",
+                slingUriBuilder -> {
+                    slingUriBuilder.setPath(null);
+                },
+                "http://example.com?queryPar=val",
+                slingUri -> {
+                    assertEquals(null, slingUri.getPath());
+                    assertEquals(null, slingUri.getResourcePath());
+                    assertEquals(null, slingUri.getSelectorString());
+                    assertEquals(null, slingUri.getExtension());
+                    assertEquals(null, slingUri.getSuffix());
+                    assertTrue(slingUri.getPathParameters().isEmpty());
+                    assertEquals("queryPar=val", slingUri.getQuery());
+                });
+    }
+
+    @Test
+    public void testAdjustReplacePathEffectivelyRemovingSelectors() {
+
+        testAdjustUri(
+                "http://example.com/test/to/path;key='val'.sel1.html/suffix/path/to/file?queryPar=val",
+                slingUriBuilder -> {
+                    slingUriBuilder.setPath("/simple/other/path");
+                },
+                "http://example.com/simple/other/path?queryPar=val",
+                slingUri -> {
+                    assertEquals("/simple/other/path", slingUri.getPath());
+                    assertEquals("/simple/other/path", slingUri.getResourcePath());
+                    assertEquals("setPath() (opposed to setResourcePath()) must also remove selectors if not present in path", null,
+                            slingUri.getSelectorString());
+                    assertEquals("setPath() (opposed to setResourcePath()) must also remove extension if not present in path", null,
+                            slingUri.getExtension());
+                    assertEquals("setPath() (opposed to setResourcePath()) must also remove suffix if not present in path", null,
+                            slingUri.getSuffix());
+                    assertTrue("setPath() (opposed to setResourcePath()) must also remove path parameters if not present in path",
+                            slingUri.getPathParameters().isEmpty());
+                    assertEquals("queryPar=val", slingUri.getQuery());
+                });
+    }
+
+    @Test
+    public void testAdjustReplacePathWithPathParametersRemovingSelector() {
+
+        testAdjustUri(
+                "http://example.com/test/to/path;key='val'.sel1.html/suffix/path/to/file?queryPar=val",
+                slingUriBuilder -> {
+                    slingUriBuilder.setPath("/simple/other/path;key='val'");
+                },
+                "http://example.com/simple/other/path;key='val'?queryPar=val",
+                slingUri -> {
+                    assertEquals("/simple/other/path;key='val'", slingUri.getPath());
+                    assertEquals("/simple/other/path", slingUri.getResourcePath());
+                    assertEquals("setPath() (opposed to setResourcePath()) must also remove selectors if not present in path", null,
+                            slingUri.getSelectorString());
+                    assertEquals("setPath() (opposed to setResourcePath()) must also remove extension if not present in path", null,
+                            slingUri.getExtension());
+                    assertEquals("setPath() (opposed to setResourcePath()) must also remove suffix if not present in path", null,
+                            slingUri.getSuffix());
+                    assertEquals(1, slingUri.getPathParameters().size());
+                    assertEquals("val", slingUri.getPathParameters().get("key"));
+                    assertEquals("queryPar=val", slingUri.getQuery());
+                });
+    }
+
+    // -- helper methods
+
+    public static void testAdjustUri(String testUri, Consumer<SlingUriBuilder> adjuster, String testUriAfterEdit,
+            Consumer<SlingUri> additionalAssertions) {
+        testAdjustUri(testUri, adjuster, testUriAfterEdit, additionalAssertions, null);
+    }
+
+    public static void testAdjustUri(String testUri, Consumer<SlingUriBuilder> adjuster, String testUriAfterEdit,
+            Consumer<SlingUri> additionalAssertions, ResourceResolver resourceResolver) {
+        SlingUri slingUri = SlingUriBuilder.parse(testUri, resourceResolver).build();
+
+        SlingUri adjustedSlingUri = slingUri.adjust(adjuster);
+
+        assertEquals(testUriAfterEdit, adjustedSlingUri.toString());
+        assertEquals(testUriAfterEdit, adjustedSlingUri.toUri().toString());
+
+        additionalAssertions.accept(adjustedSlingUri);
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/api/uri/SlingUriInvalidUrisTest.java b/src/test/java/org/apache/sling/api/uri/SlingUriInvalidUrisTest.java
new file mode 100644
index 0000000..48a9aff
--- /dev/null
+++ b/src/test/java/org/apache/sling/api/uri/SlingUriInvalidUrisTest.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.api.uri;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.Collection;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class SlingUriInvalidUrisTest {
+
+    @Parameters(name = "Invalid URI: {0}")
+    public static Collection<String> data() {
+        return Arrays.asList(":foo", "https://", "https:", "@:", "://", "::::");
+    }
+
+    private final String invalidUri;
+
+    public SlingUriInvalidUrisTest(String invalidUri) {
+        this.invalidUri = invalidUri;
+    }
+
+    @Test
+    public void testInvalidUriToStringIsUnchanged() {
+        try {
+            new URI(invalidUri);
+            fail("URI " + invalidUri + " is not invalid");
+        } catch (URISyntaxException e) {
+            assertEquals("Invalid URI " + invalidUri + "(e=" + e + ") is unchanged for SlingUriBuilder parse/toString",
+                    invalidUri,
+                    SlingUriBuilder.parse(invalidUri, null).build().toString());
+        }
+    }
+
+    @Test
+    public void testAdjustInvalidUriNoEffect() {
+
+        SlingUri slingUri = SlingUriBuilder.parse(invalidUri, null).build();
+        SlingUri slingUriAdjusted = slingUri.adjust(b -> b.setResourcePath("/test"));
+        assertNull("setResourcePath() should have been ignored for uri " + invalidUri, slingUriAdjusted.getResourcePath());
+    }
+
+    @Test
+    public void testAdjustInvalidUriToValidUri() {
+
+        SlingUri slingUri = SlingUriBuilder.parse(invalidUri, null).build();
+        SlingUri slingUriAdjusted = slingUri.adjust(b -> b.setSchemeSpecificPart(null).setResourcePath("/test"));
+        assertEquals("Using setSchemeSpecificPart(null) should reset the invalid URI to be adjustable", "/test",
+                slingUriAdjusted.getResourcePath());
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/api/uri/SlingUriRebaseTest.java b/src/test/java/org/apache/sling/api/uri/SlingUriRebaseTest.java
new file mode 100644
index 0000000..896a686
--- /dev/null
+++ b/src/test/java/org/apache/sling/api/uri/SlingUriRebaseTest.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.uri;
+
+import static org.apache.sling.api.uri.SlingUriTest.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 SlingUriRebaseTest {
+
+    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, slingUri -> {
+            assertEquals("/test/to/file", slingUri.getResourcePath());
+            assertEquals(null, slingUri.getSelectorString());
+            assertEquals(null, slingUri.getExtension());
+            assertEquals(null, slingUri.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, slingUri -> {
+            assertEquals("/test/to/file.css", slingUri.getResourcePath());
+            assertEquals(null, slingUri.getSelectorString());
+            assertEquals(null, slingUri.getExtension());
+            assertEquals(null, slingUri.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, slingUri -> {
+            assertEquals("/path/to/page", slingUri.getResourcePath());
+            assertEquals(null, slingUri.getSelectorString());
+            assertEquals("html", slingUri.getExtension());
+            assertEquals(null, slingUri.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, slingUri -> {
+            assertEquals("/test/to/file.ext.sel1.json/suffix/path.js", slingUri.getResourcePath());
+            assertEquals(null, slingUri.getSelectorString());
+            assertEquals(null, slingUri.getExtension());
+            assertEquals(null, slingUri.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, slingUri -> {
+            assertEquals("/test/to/file.ext.sel1.json/suffix/path", slingUri.getResourcePath());
+            assertEquals(null, slingUri.getSelectorString());
+            assertEquals("js", slingUri.getExtension());
+            assertEquals(null, slingUri.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, slingUri -> {
+            assertEquals("/test/to/file", slingUri.getResourcePath());
+            assertEquals("ext.sel1", slingUri.getSelectorString());
+            assertEquals("json", slingUri.getExtension());
+            assertEquals("/suffix/path.js", slingUri.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, slingUri -> {
+            assertEquals("/test/to/file.ext.sel1", slingUri.getResourcePath());
+            assertEquals(null, slingUri.getSelectorString());
+            assertEquals("json", slingUri.getExtension());
+            assertEquals("/suffix/path.js", slingUri.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, slingUri -> {
+            assertEquals("/test/to/file.ext", slingUri.getResourcePath());
+            assertEquals("sel1", slingUri.getSelectorString());
+            assertEquals("json", slingUri.getExtension());
+            assertEquals("/suffix/path.js", slingUri.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, slingUri -> {
+            assertEquals("/test/to/file", slingUri.getResourcePath());
+            assertEquals("ext.sel1", slingUri.getSelectorString());
+            assertEquals("json", slingUri.getExtension());
+            assertEquals("/suffix/path.js", slingUri.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, slingUri -> {
+            assertEquals("/test/to/file.ext.sel1", slingUri.getResourcePath());
+            assertEquals(null, slingUri.getSelectorString());
+            assertEquals("json", slingUri.getExtension());
+            assertEquals("/suffix/path.js", slingUri.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, slingUri -> {
+            assertEquals("/test/to/file", slingUri.getResourcePath());
+            assertEquals("ext.sel1", slingUri.getSelectorString());
+            assertEquals("json", slingUri.getExtension());
+            assertEquals("/suffix/path.js", slingUri.getSuffix());
+        }, resolver);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testRebaseNotAllowedWithoutResolver() throws URISyntaxException {
+
+        String testPath = "/path/to/page.html";
+        SlingUriBuilder.parse(testPath, null)
+                .rebaseResourcePath()
+                .build();
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/api/uri/SlingUriTest.java b/src/test/java/org/apache/sling/api/uri/SlingUriTest.java
new file mode 100644
index 0000000..8faaa71
--- /dev/null
+++ b/src/test/java/org/apache/sling/api/uri/SlingUriTest.java
@@ -0,0 +1,443 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.api.uri;
+
+import static java.util.Arrays.asList;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import java.net.URI;
+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 SlingUriTest {
+
+    @Mock
+    ResourceResolver resolver;
+
+    @Test
+    public void testFullSlingUri() {
+
+        String testUriStr = "http://host.com/test/to/path.html";
+        testUri(testUriStr, false, false, false, true, false, slingUri -> {
+            assertEquals("http", slingUri.getScheme());
+            assertEquals("//host.com/test/to/path.html", slingUri.getSchemeSpecificPart());
+            assertEquals(null, slingUri.getUserInfo());
+            assertEquals("host.com", slingUri.getHost());
+            assertEquals(-1, slingUri.getPort());
+            assertEquals("/test/to/path", slingUri.getResourcePath());
+            assertEquals(null, slingUri.getSelectorString());
+            assertArrayEquals(new String[] {}, slingUri.getSelectors());
+            assertEquals("html", slingUri.getExtension());
+            assertEquals(null, slingUri.getSuffix());
+            assertEquals(null, slingUri.getQuery());
+            assertEquals(null, slingUri.getFragment());
+        }, asList(resolver, null));
+
+    }
+
+    @Test
+    public void testFullSlingUriComplex() {
+
+        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, slingUri -> {
+            assertEquals("https", slingUri.getScheme());
+            assertEquals("//test:pw@host.com:888/test/to/path.sel1.json/suffix/path?p1=2&p2=3", slingUri.getSchemeSpecificPart());
+            assertEquals("test:pw", slingUri.getUserInfo());
+            assertEquals("host.com", slingUri.getHost());
+            assertEquals(888, slingUri.getPort());
+            assertEquals("/test/to/path", slingUri.getResourcePath());
+            assertEquals("sel1", slingUri.getSelectorString());
+            assertArrayEquals(new String[] { "sel1" }, slingUri.getSelectors());
+            assertEquals("json", slingUri.getExtension());
+            assertEquals("/suffix/path", slingUri.getSuffix());
+            assertEquals("p1=2&p2=3", slingUri.getQuery());
+            assertEquals("frag3939", slingUri.getFragment());
+        }, asList(resolver, null));
+
+    }
+
+    @Test
+    public void testAbsolutePathSlingUri() {
+        String testUriStr = "/test/to/path.sel1.json/suffix/path?p1=2&p2=3#frag3939";
+
+        testUri(testUriStr, true, true, false, false, false, slingUri -> {
+            assertEquals(null, slingUri.getScheme());
+            assertEquals(null, slingUri.getUserInfo());
+            assertEquals(null, slingUri.getHost());
+            assertEquals(-1, slingUri.getPort());
+            assertEquals("/test/to/path", slingUri.getResourcePath());
+            assertEquals("sel1", slingUri.getSelectorString());
+            assertEquals("json", slingUri.getExtension());
+            assertEquals("/suffix/path", slingUri.getSuffix());
+            assertEquals("p1=2&p2=3", slingUri.getQuery());
+            assertEquals("frag3939", slingUri.getFragment());
+        }, asList(resolver, null));
+    }
+
+    @Test
+    public void testSlingUriSuffixWithDots() {
+
+        String testUriStr = "/test/to/path.min.js/suffix/app.nodesbrowser.js";
+        testUri(testUriStr, true, true, false, false, false, slingUri -> {
+            assertEquals(null, slingUri.getScheme());
+            assertEquals(null, slingUri.getUserInfo());
+            assertEquals(null, slingUri.getHost());
+            assertEquals(-1, slingUri.getPort());
+            assertEquals("/test/to/path", slingUri.getResourcePath());
+            assertEquals("min", slingUri.getSelectorString());
+            assertEquals("js", slingUri.getExtension());
+            assertEquals("/suffix/app.nodesbrowser.js", slingUri.getSuffix());
+            assertEquals(null, slingUri.getQuery());
+            assertEquals(null, slingUri.getFragment());
+        }, asList(resolver, null));
+    }
+
+    @Test
+    public void testSlingUriMultipleDots() {
+
+        String testUriStr = "/test/to/path.sel1.sel2..sel4.js";
+        testUri(testUriStr, true, true, false, false, false, slingUri -> {
+            assertEquals(null, slingUri.getScheme());
+            assertEquals(null, slingUri.getUserInfo());
+            assertEquals(null, slingUri.getHost());
+            assertEquals(-1, slingUri.getPort());
+            assertEquals("/test/to/path", slingUri.getResourcePath());
+            assertEquals(4, slingUri.getSelectors().length);
+            assertEquals("sel1.sel2..sel4", slingUri.getSelectorString());
+            assertArrayEquals(new String[] { "sel1", "sel2", "", "sel4" }, slingUri.getSelectors());
+            assertEquals("js", slingUri.getExtension());
+            assertEquals(null, slingUri.getSuffix());
+            assertEquals(null, slingUri.getQuery());
+            assertEquals(null, slingUri.getFragment());
+        }, asList(resolver, null));
+
+        String testUriStr2 = "/test/to/path.sel1.sel2../sel4.js";
+        testUri(testUriStr2, true, true, false, false, false, slingUri -> {
+            assertEquals(null, slingUri.getScheme());
+            assertEquals(null, slingUri.getUserInfo());
+            assertEquals(null, slingUri.getHost());
+            assertEquals(-1, slingUri.getPort());
+            assertEquals("/test/to/path", slingUri.getResourcePath());
+            assertEquals(1, slingUri.getSelectors().length);
+            assertEquals("sel1", slingUri.getSelectorString());
+            assertEquals("sel2", slingUri.getExtension());
+            assertEquals("/sel4.js", slingUri.getSuffix());
+            assertEquals(null, slingUri.getQuery());
+            assertEquals(null, slingUri.getFragment());
+        }, null, true);
+    }
+
+    @Test
+    public void testRelativePathSlingUri() {
+        String testUriStr = "../path.html#frag1";
+
+        testUri(testUriStr, true, false, true, false, false, slingUri -> {
+            assertEquals(null, slingUri.getScheme());
+            assertEquals(null, slingUri.getUserInfo());
+            assertEquals(null, slingUri.getHost());
+            assertEquals(-1, slingUri.getPort());
+            assertEquals("../path", slingUri.getResourcePath());
+            assertEquals(null, slingUri.getSelectorString());
+            assertEquals("html", slingUri.getExtension());
+            assertEquals(null, slingUri.getSuffix());
+            assertEquals(null, slingUri.getQuery());
+            assertEquals("frag1", slingUri.getFragment());
+        }, asList(resolver, null));
+    }
+
+    @Test
+    public void testRelativePathSlingUriComplex() {
+        String testUriStr = "../path/./deep/path/../path.sel1.sel2.html?test=1#frag1";
+
+        testUri(testUriStr, true, false, true, false, false, slingUri -> {
+            assertEquals(null, slingUri.getScheme());
+            assertEquals(null, slingUri.getUserInfo());
+            assertEquals(null, slingUri.getHost());
+            assertEquals(-1, slingUri.getPort());
+            assertEquals("../path/./deep/path/../path", slingUri.getResourcePath());
+            assertEquals("sel1.sel2", slingUri.getSelectorString());
+            assertArrayEquals(new String[] { "sel1", "sel2" }, slingUri.getSelectors());
+            assertEquals("html", slingUri.getExtension());
+            assertEquals(null, slingUri.getSuffix());
+            assertEquals("test=1", slingUri.getQuery());
+            assertEquals("frag1", slingUri.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, slingUri -> {
+            assertEquals(null, slingUri.getScheme());
+            assertEquals(null, slingUri.getUserInfo());
+            assertEquals(null, slingUri.getHost());
+            assertEquals(-1, slingUri.getPort());
+            assertEquals("/test/to/path", slingUri.getResourcePath());
+            assertEquals("sel1", slingUri.getSelectorString());
+            assertEquals("html", slingUri.getExtension());
+            assertEquals(1, slingUri.getPathParameters().size());
+            assertEquals("1.0", slingUri.getPathParameters().get("v"));
+            assertEquals("/suffix/path", slingUri.getSuffix());
+            assertEquals("p1=2&p2=3", slingUri.getQuery());
+            assertEquals("frag3939", slingUri.getFragment());
+        }, asList(resolver, null));
+
+        String testUriStr2 = "/test/to/file;foo='bar'.sel1.sel2.json/suffix/path";
+        testUri(testUriStr2, true, true, false, false, false, slingUri -> {
+            assertEquals(null, slingUri.getScheme());
+            assertEquals(null, slingUri.getUserInfo());
+            assertEquals(null, slingUri.getHost());
+            assertEquals(-1, slingUri.getPort());
+            assertEquals("/test/to/file", slingUri.getResourcePath());
+            assertEquals("sel1.sel2", slingUri.getSelectorString());
+            assertEquals("json", slingUri.getExtension());
+            assertEquals(1, slingUri.getPathParameters().size());
+            assertEquals("bar", slingUri.getPathParameters().get("foo"));
+            assertEquals("/suffix/path", slingUri.getSuffix());
+            assertEquals(null, slingUri.getQuery());
+            assertEquals(null, slingUri.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, slingUri -> {
+            assertEquals(null, slingUri.getScheme());
+            assertEquals(null, slingUri.getUserInfo());
+            assertEquals(null, slingUri.getHost());
+            assertEquals(-1, slingUri.getPort());
+            assertEquals("/test/to/path", slingUri.getResourcePath());
+            assertEquals("sel1", slingUri.getSelectorString());
+            assertEquals("html", slingUri.getExtension());
+
+            assertEquals(3, slingUri.getPathParameters().size());
+            assertEquals("1.0", slingUri.getPathParameters().get("v"));
+            assertEquals("test/nested", slingUri.getPathParameters().get("antotherParam"));
+            assertEquals("7", slingUri.getPathParameters().get("antotherParam2"));
+
+            assertEquals("/suffix/path", slingUri.getSuffix());
+            assertEquals("p1=2&p2=3", slingUri.getQuery());
+            assertEquals("frag3939", slingUri.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";
+
+        SlingUri testUri = testUri(testUriStr, true, true, false, false, false, slingUri -> {
+            assertEquals(null, slingUri.getScheme());
+            assertEquals(null, slingUri.getUserInfo());
+            assertEquals(null, slingUri.getHost());
+            assertEquals(-1, slingUri.getPort());
+            assertEquals("/test/to/path", slingUri.getResourcePath());
+            assertEquals("sel1", slingUri.getSelectorString());
+            assertEquals("html", slingUri.getExtension());
+            assertEquals(1, slingUri.getPathParameters().size());
+            assertEquals("1.0", slingUri.getPathParameters().get("v"));
+            assertEquals("/suffix/path", slingUri.getSuffix());
+            assertEquals("p1=2&p2=3", slingUri.getQuery());
+            assertEquals("frag3939", slingUri.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, true, true, slingUri -> {
+            assertEquals("javascript", slingUri.getScheme());
+            assertEquals(null, slingUri.getUserInfo());
+            assertEquals(null, slingUri.getHost());
+            assertEquals(-1, slingUri.getPort());
+            assertEquals(null, slingUri.getSelectorString());
+            assertEquals(null, slingUri.getExtension());
+            assertEquals(null, slingUri.getSuffix());
+            assertEquals("void(0)", slingUri.getSchemeSpecificPart());
+            assertEquals(null, slingUri.getQuery());
+            assertEquals(null, slingUri.getFragment());
+        }, asList(resolver, null));
+    }
+
+    @Test
+    public void testMailtotUri() {
+        String testUriStr = "mailto:jon.doe@example.com";
+
+        testUri(testUriStr, false, false, false, true, true, slingUri -> {
+            assertEquals("mailto", slingUri.getScheme());
+            assertEquals(null, slingUri.getUserInfo());
+            assertEquals(null, slingUri.getHost());
+            assertEquals(-1, slingUri.getPort());
+            assertEquals(null, slingUri.getSelectorString());
+            assertEquals(null, slingUri.getExtension());
+            assertEquals(null, slingUri.getSuffix());
+            assertEquals("jon.doe@example.com", slingUri.getSchemeSpecificPart());
+            assertEquals(null, slingUri.getQuery());
+            assertEquals(null, slingUri.getFragment());
+        }, asList(resolver, null));
+    }
+
+    @Test
+    public void testHashOnlyUri() {
+
+        testUri("#", false, false, false, false, false, slingUri -> {
+            assertEquals(null, slingUri.getScheme());
+            assertEquals(null, slingUri.getUserInfo());
+            assertEquals(null, slingUri.getHost());
+            assertEquals(-1, slingUri.getPort());
+            assertEquals(null, slingUri.getResourcePath());
+            assertEquals(null, slingUri.getSelectorString());
+            assertEquals(null, slingUri.getExtension());
+            assertEquals(null, slingUri.getSuffix());
+            assertEquals(null, slingUri.getQuery());
+            assertEquals("", slingUri.getFragment());
+        }, asList(resolver, null));
+
+        testUri("#fragment", false, false, false, false, false, slingUri -> {
+            assertEquals(null, slingUri.getScheme());
+            assertEquals(null, slingUri.getUserInfo());
+            assertEquals(null, slingUri.getHost());
+            assertEquals(-1, slingUri.getPort());
+            assertEquals(null, slingUri.getResourcePath());
+            assertEquals(null, slingUri.getSelectorString());
+            assertEquals(null, slingUri.getExtension());
+            assertEquals(null, slingUri.getSuffix());
+            assertEquals(null, slingUri.getQuery());
+            assertEquals("fragment", slingUri.getFragment());
+        }, asList(resolver, null));
+    }
+
+    @Test
+    public void testQueryOnlyUri() {
+
+        testUri("?", false, false, false, false, false, slingUri -> {
+            assertEquals(null, slingUri.getScheme());
+            assertEquals(null, slingUri.getUserInfo());
+            assertEquals(null, slingUri.getHost());
+            assertEquals(-1, slingUri.getPort());
+            assertEquals(null, slingUri.getResourcePath());
+            assertEquals(null, slingUri.getSelectorString());
+            assertEquals(null, slingUri.getExtension());
+            assertEquals(null, slingUri.getSuffix());
+            assertEquals("", slingUri.getQuery());
+            assertEquals(null, slingUri.getFragment());
+        }, asList(resolver, null));
+
+        testUri("?test=test", false, false, false, false, false, slingUri -> {
+            assertEquals(null, slingUri.getScheme());
+            assertEquals(null, slingUri.getUserInfo());
+            assertEquals(null, slingUri.getHost());
+            assertEquals(-1, slingUri.getPort());
+            assertEquals(null, slingUri.getResourcePath());
+            assertEquals(null, slingUri.getSelectorString());
+            assertEquals(null, slingUri.getExtension());
+            assertEquals(null, slingUri.getSuffix());
+            assertEquals("test=test", slingUri.getQuery());
+            assertEquals(null, slingUri.getFragment());
+        }, asList(resolver, null));
+    }
+
+    @Test
+    public void testUnusualQueryFragmentCombinations() {
+        testUri("?#", false, false, false, false, false, slingUri -> {
+            assertEquals("", slingUri.getQuery());
+            assertEquals("", slingUri.getFragment());
+        }, asList(resolver, null));
+        testUri("?t=2#", false, false, false, false, false, slingUri -> {
+            assertEquals("t=2", slingUri.getQuery());
+            assertEquals("", slingUri.getFragment());
+        }, asList(resolver, null));
+        testUri("?#t=3", false, false, false, false, false, slingUri -> {
+            assertEquals("", slingUri.getQuery());
+            assertEquals("t=3", slingUri.getFragment());
+        }, asList(resolver, null));
+        testUri("", false, false, false, false, false, slingUri -> {
+            assertEquals(null, slingUri.getQuery());
+            assertEquals(null, slingUri.getFragment());
+        }, asList(resolver, null));
+    }
+
+    // -- helper methods
+    public static void testUri(String testUri, boolean isPath, boolean isAbsolutePath, boolean isRelativePath, boolean isAbsolute,
+            boolean isOpaque, Consumer<SlingUri> additionalAssertions, List<ResourceResolver> resourceResolvers) {
+        for (ResourceResolver rr : resourceResolvers) {
+            testUri(testUri, isPath, isAbsolutePath, isRelativePath, isAbsolute, isOpaque, additionalAssertions, rr);
+        }
+    }
+
+    public static SlingUri testUri(String testUri, boolean isPath, boolean isAbsolutePath, boolean isRelativePath, boolean isAbsolute,
+            boolean isOpaque, Consumer<SlingUri> additionalAssertions) {
+        return testUri(testUri, isPath, isAbsolutePath, isRelativePath, isAbsolute, isOpaque, additionalAssertions,
+                (ResourceResolver) null);
+    }
+
+    public static SlingUri testUri(String testUri, boolean isPath, boolean isAbsolutePath, boolean isRelativePath, boolean isAbsolute,
+            boolean isOpaque, Consumer<SlingUri> additionalAssertions, ResourceResolver resourceResolver) {
+        return testUri(testUri, isPath, isAbsolutePath, isRelativePath, isAbsolute, isOpaque, additionalAssertions, resourceResolver,
+                false);
+    }
+
+    public static SlingUri testUri(String testUri, boolean isPath, boolean isAbsolutePath, boolean isRelativePath, boolean isAbsolute,
+            boolean isOpaque, Consumer<SlingUri> additionalAssertions, ResourceResolver resourceResolver, boolean urlIsRestructured) {
+        SlingUri slingUri = SlingUriBuilder.parse(testUri, resourceResolver).build();
+
+        if (!urlIsRestructured) {
+            assertEquals("Uri toString() same as input", testUri, slingUri.toString());
+            assertEquals("Uri toUri().toString() same as input", testUri, slingUri.toUri().toString());
+        }
+
+        assertEquals("isPath()", isPath, slingUri.isPath());
+        assertEquals("isAbsolutePath()", isAbsolutePath, slingUri.isAbsolutePath());
+        assertEquals("isRelativePath()", isRelativePath, slingUri.isRelativePath());
+        assertEquals("isAbsolute()", isAbsolute, slingUri.isAbsolute());
+        assertEquals("isOpaque()", isOpaque, slingUri.isOpaque());
+
+        URI javaUri = slingUri.toUri();
+        assertEquals("isOpaque() matches to java URI impl", javaUri.isOpaque(), slingUri.isOpaque());
+        assertEquals("getSchemeSpecificPart() matches to java URI impl", javaUri.getSchemeSpecificPart(),
+                slingUri.getSchemeSpecificPart());
+        assertEquals("getFragment() matches to java URI impl", javaUri.getFragment(), slingUri.getFragment());
+        assertEquals("getQuery() matches to java URI impl", javaUri.getQuery(), slingUri.getQuery());
+        assertEquals("isAbsolute() matches to java URI impl", javaUri.isAbsolute(), slingUri.isAbsolute());
+
+        additionalAssertions.accept(slingUri);
+
+        SlingUri slingUriParsedFromSameInput = SlingUriBuilder.parse(testUri, resourceResolver).build();
+        assertEquals("uris parsed from same input are expected to be equal", slingUriParsedFromSameInput, slingUri);
+        assertEquals("uris parsed from same input are expected to have the same hash code", slingUriParsedFromSameInput.hashCode(),
+                slingUri.hashCode());
+
+        return slingUri;
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/api/uri/SlingUriToSlingRequestPathInfoCompatibilityTest.java b/src/test/java/org/apache/sling/api/uri/SlingUriToSlingRequestPathInfoCompatibilityTest.java
new file mode 100644
index 0000000..851f957
--- /dev/null
+++ b/src/test/java/org/apache/sling/api/uri/SlingUriToSlingRequestPathInfoCompatibilityTest.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.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 SlingUriToSlingRequestPathInfoCompatibilityTest {
+
+    @Mock
+    ResourceResolver resourceResolver;
+
+    @Mock
+    Resource resource;
+
+    private RequestPathInfo createSlingUri(String resolutionPath, String resolutionPathInfo) {
+        when(resourceResolver.getResource(resolutionPath)).thenReturn(resource);
+        return SlingUriBuilder.parse(resolutionPath + (resolutionPathInfo != null ? resolutionPathInfo : ""), resourceResolver).build();
+    }
+
+    @Test
+    public void testTrailingDot() {
+        RequestPathInfo p = createSlingUri("/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 = createSlingUri("/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 = createSlingUri("/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 = createSlingUri("/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 = createSlingUri("/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 = createSlingUri("/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 = createSlingUri("/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 = createSlingUri("/", 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 = createSlingUri("/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 = createSlingUri("/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 = createSlingUri("/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 = createSlingUri("/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 = createSlingUri("/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 = createSlingUri("/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 = createSlingUri("/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 = createSlingUri("/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 = createSlingUri("/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 = createSlingUri("/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 = createSlingUri("/bunkai", ".1.json");
+        assertEquals("/bunkai", p.getResourcePath());
+        assertEquals("json", p.getExtension());
+        assertEquals("1", p.getSelectorString());
+    }
+
+    @Test
+    public void testJIRA_250_b() {
+        RequestPathInfo p = createSlingUri("/", ".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 = createSlingUri("/", ".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 = createSlingUri("/", ".json");
+        assertEquals("/", p.getResourcePath());
+        assertEquals("json", p.getExtension());
+        assertNull("Suffix is null", p.getSuffix());
+        assertNull("Selectors are null", p.getSelectorString());
+    }
+
+}