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/10 22:12:41 UTC

[sling-org-apache-sling-api] branch feature/SLING-9662-Introduce-Resource-Mapping-SPI-v2 created (now c27db47)

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

ghenzler pushed a change to branch feature/SLING-9662-Introduce-Resource-Mapping-SPI-v2
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-api.git.


      at c27db47  SLING-9662 Introduce Resource Mapping SPI incl. ResourceUri

This branch includes the following new commits:

     new c27db47  SLING-9662 Introduce Resource Mapping SPI incl. ResourceUri

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



[sling-org-apache-sling-api] 01/01: SLING-9662 Introduce Resource Mapping SPI incl. ResourceUri

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

ghenzler pushed a commit to branch feature/SLING-9662-Introduce-Resource-Mapping-SPI-v2
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-api.git

commit c27db47a07ce7d85896e1c761f462653a7532e79
Author: georg.henzler <ge...@netcentric.biz>
AuthorDate: Mon Sep 7 16:01:00 2020 +0200

    SLING-9662 Introduce Resource Mapping SPI incl. ResourceUri
---
 pom.xml                                            |   2 +-
 .../sling/api/resource/ResourceResolver.java       |   6 +-
 .../resource/mapping/PathToUriMappingService.java  |  69 +++
 .../sling/api/resource/mapping/package-info.java   |   2 +-
 .../apache/sling/api/resource/package-info.java    |   2 +-
 .../apache/sling/api/resource/uri/ResourceUri.java | 122 ++++
 .../sling/api/resource/uri/ResourceUriBuilder.java | 607 ++++++++++++++++++
 .../sling/api/resource/{ => uri}/package-info.java |   5 +-
 .../spi/resource/mapping/MappingChainContext.java  |  51 ++
 .../spi/resource/mapping/ResourceUriMapper.java    |  56 ++
 .../resource/mapping/package-info.java             |   4 +-
 .../sling/api/resource/uri/ResourceUriTest.java    | 679 +++++++++++++++++++++
 ...UriToSlingRequestPathInfoCompatibilityTest.java | 271 ++++++++
 13 files changed, 1866 insertions(+), 10 deletions(-)

diff --git a/pom.xml b/pom.xml
index 8e02cb7..10f3109 100644
--- a/pom.xml
+++ b/pom.xml
@@ -105,7 +105,7 @@
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-lang3</artifactId>
             <version>3.2</version>
-            <scope>test</scope>
+            
         </dependency>
         <dependency>
             <groupId>org.osgi</groupId>
diff --git a/src/main/java/org/apache/sling/api/resource/ResourceResolver.java b/src/main/java/org/apache/sling/api/resource/ResourceResolver.java
index 8a2b27d..e92abe0 100644
--- a/src/main/java/org/apache/sling/api/resource/ResourceResolver.java
+++ b/src/main/java/org/apache/sling/api/resource/ResourceResolver.java
@@ -22,12 +22,14 @@ import java.io.Closeable;
 import java.util.Iterator;
 import java.util.Map;
 
-import org.jetbrains.annotations.Nullable;
-import org.jetbrains.annotations.NotNull;
 import javax.servlet.http.HttpServletRequest;
 
 import org.apache.sling.api.adapter.Adaptable;
 import org.apache.sling.api.resource.mapping.ResourceMapper;
+import org.apache.sling.api.resource.uri.ResourceUri;
+import org.apache.sling.api.resource.uri.ResourceUriBuilder;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 import org.osgi.annotation.versioning.ProviderType;
 
 /**
diff --git a/src/main/java/org/apache/sling/api/resource/mapping/PathToUriMappingService.java b/src/main/java/org/apache/sling/api/resource/mapping/PathToUriMappingService.java
new file mode 100644
index 0000000..8134699
--- /dev/null
+++ b/src/main/java/org/apache/sling/api/resource/mapping/PathToUriMappingService.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.api.resource.mapping;
+
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.sling.api.resource.uri.ResourceUri;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.osgi.annotation.versioning.ProviderType;
+
+/** Provides a way to resolve URIs to resource paths and map resource paths to URIs. */
+@ProviderType
+public interface PathToUriMappingService {
+
+    /** A context hint for a consumer of the map/resolve result */
+    @ProviderType
+    public interface ContextHint {
+        String getName();
+    }
+
+    /** The result of a map or resolve operation */
+    @ProviderType
+    public interface Result {
+        /** @return the ResourceUri */
+        @NotNull
+        ResourceUri getResourceUri();
+
+        /** @return context hints (e.g. 'invalid link' for map(), or 'requires authentication' for resolve()) */
+        @NotNull
+        Set<ContextHint> getContextHints();
+
+        /** @return all intermediate mappings as produced by {@link org.apache.sling.spi.resource.mapping.ResourceUriMapper} services. */
+        @NotNull
+        Map<String, ResourceUri> getIntermediateMappings();
+    }
+
+    /** Resolves a path relative to the given request.
+     * 
+     * @param request
+     * @param path
+     * @return a @{link PathToUriMappingService.Result} */
+    Result resolve(@Nullable HttpServletRequest request, @NotNull String path);
+
+    /** @param request
+     * @param resourcePath
+     * @return a @{link PathToUriMappingService.Result} */
+    Result map(@Nullable HttpServletRequest request, @NotNull String resourcePath);
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/api/resource/mapping/package-info.java b/src/main/java/org/apache/sling/api/resource/mapping/package-info.java
index e871225..00a1ea1 100644
--- a/src/main/java/org/apache/sling/api/resource/mapping/package-info.java
+++ b/src/main/java/org/apache/sling/api/resource/mapping/package-info.java
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-@Version("1.0.1")
+@Version("1.1.0")
 package org.apache.sling.api.resource.mapping;
 
 import org.osgi.annotation.versioning.Version;
diff --git a/src/main/java/org/apache/sling/api/resource/package-info.java b/src/main/java/org/apache/sling/api/resource/package-info.java
index 7bd85e6..ac05b61 100644
--- a/src/main/java/org/apache/sling/api/resource/package-info.java
+++ b/src/main/java/org/apache/sling/api/resource/package-info.java
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-@Version("2.12.2")
+@Version("2.13.0")
 package org.apache.sling.api.resource;
 
 import org.osgi.annotation.versioning.Version;
diff --git a/src/main/java/org/apache/sling/api/resource/uri/ResourceUri.java b/src/main/java/org/apache/sling/api/resource/uri/ResourceUri.java
new file mode 100644
index 0000000..858ac31
--- /dev/null
+++ b/src/main/java/org/apache/sling/api/resource/uri/ResourceUri.java
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.api.resource.uri;
+
+import static org.apache.commons.lang3.StringUtils.isBlank;
+import static org.apache.commons.lang3.StringUtils.isNotBlank;
+
+import java.net.URI;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import org.apache.sling.api.request.RequestPathInfo;
+import org.apache.sling.api.resource.Resource;
+
+/** Represents an immutable URI that points to a resource or alternatively, can contain special URIs like {@code mailto:} or
+ * {@code javascript:}. */
+public interface ResourceUri extends RequestPathInfo {
+
+    /** @return returns the URI. */
+    public URI toUri();
+
+    /** @return returns the URI as String. */
+    public String toString();
+
+    /** @return returns the scheme of the link */
+    public String getScheme();
+
+    /** @return returns the user info of the link */
+    public String getUserInfo();
+
+    /** @return returns the host of the link */
+    public String getHost();
+
+    /** @return returns the port of the link */
+    public int getPort();
+
+    /** @return returns the resource path or null if link does not contain path. */
+    @Override
+    public String getResourcePath();
+
+    /** @return returns the selector string */
+    @Override
+    public String getSelectorString();
+
+    /** @return returns the selector array */
+    @Override
+    public String[] getSelectors();
+
+    /** @return returns the extension of the link */
+    @Override
+    public String getExtension();
+
+    /** @return returns the path parameters */
+    public Map<String, String> getPathParameters();
+
+    /** @return returns the suffix of the link */
+    @Override
+    public String getSuffix();
+
+    /** @return returns the query part of the link */
+    public String getQuery();
+
+    /** @return returns the url fragment of the link */
+    public String getFragment();
+
+    /** @return scheme specific part of the URL */
+    public String getSchemeSpecificPart();
+    
+    /** @return returns the corresponding */
+    @Override
+    public Resource getSuffixResource();
+
+    /** @return returns true if the link is either a relative or absolute path (this is the case if scheme and host is empty and the URI
+     *         path is set) */
+    default boolean isPath() {
+        return isBlank(getScheme())
+                && isBlank(getHost())
+                && isNotBlank(getResourcePath());
+    }
+
+    /** @return true if the link is a absolute path starting with a slash ('/'). This is the default case for all links to pages and assets
+     *         in AEM. */
+    default boolean isAbsolutePath() {
+        return isPath() && getResourcePath().startsWith(ResourceUriBuilder.CHAR_SLASH);
+    }
+
+    /** @return true if link is relative (not an URL and not starting with '/') */
+    default boolean isRelativePath() {
+        return isPath() && !getResourcePath().startsWith(ResourceUriBuilder.CHAR_SLASH);
+    }
+
+    /** @return true if the link is an absolute URI containing a scheme. */
+    default boolean isFullUri() {
+        return isNotBlank(getScheme())
+                && isNotBlank(getHost());
+    }
+
+    /** @param builderConsumer
+     * @return the adjusted ResourceUri (new instance) */
+    default ResourceUri adjust(Consumer<ResourceUriBuilder> builderConsumer) {
+        ResourceUriBuilder builder = ResourceUriBuilder.createFrom(this);
+        builderConsumer.accept(builder);
+        return builder.build();
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/api/resource/uri/ResourceUriBuilder.java b/src/main/java/org/apache/sling/api/resource/uri/ResourceUriBuilder.java
new file mode 100644
index 0000000..bc9c4c6
--- /dev/null
+++ b/src/main/java/org/apache/sling/api/resource/uri/ResourceUriBuilder.java
@@ -0,0 +1,607 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.api.resource.uri;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.request.RequestPathInfo;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+
+public class ResourceUriBuilder {
+
+    static final String CHAR_HASH = "#";
+    static final String CHAR_QM = "?";
+    static final String CHAR_DOT = ".";
+    static final String CHAR_SLASH = "/";
+    static final String CHAR_AT = "@";
+    static final String SELECTOR_DOT_REGEX = "\\.(?!\\.?/)"; // (?!\\.?/) to avoid matching ./ and ../
+    static final String CHAR_COLON = ":";
+    static final String CHAR_SEMICOLON = ";";
+    static final String CHAR_EQUALS = "=";
+    static final String CHAR_SINGLEQUOTE = "'";
+
+    public static ResourceUriBuilder create() {
+        return new ResourceUriBuilder();
+    }
+
+    /** Creates a builder from another ResourceUri.
+     * 
+     * @param resourceUri
+     * @return a ResourceUriBuilder */
+    public static ResourceUriBuilder createFrom(ResourceUri resourceUri) {
+        return create()
+                .setScheme(resourceUri.getScheme())
+                .setUserInfo(resourceUri.getUserInfo())
+                .setHost(resourceUri.getHost())
+                .setPort(resourceUri.getPort())
+                .setResourcePath(resourceUri.getResourcePath())
+                .setPathParameters(resourceUri.getPathParameters())
+                .setSelectors(resourceUri.getSelectors())
+                .setExtension(resourceUri.getExtension())
+                .setSuffix(resourceUri.getSuffix())
+                .setQuery(resourceUri.getQuery())
+                .setFragment(resourceUri.getFragment())
+                .setSchemeSpecificPart(resourceUri.getSchemeSpecificPart())
+                .setResourceResolver(resourceUri instanceof ImmutableResourceUri
+                        ? ((ImmutableResourceUri) resourceUri).getBuilder().resourceResolver
+                        : null);
+    }
+
+    /** Creates a builder from a Resource (only taking the resource path into account).
+     * 
+     * @param resource
+     * @return a ResourceUriBuilder */
+    public static ResourceUriBuilder createFrom(Resource resource) {
+        return create()
+                .setResourcePath(resource.getPath())
+                .setResourceResolver(resource.getResourceResolver());
+    }
+
+    /** Creates a builder from a RequestPathInfo instance .
+     * 
+     * @param requestPathInfo
+     * @return a ResourceUriBuilder */
+    public static ResourceUriBuilder createFrom(RequestPathInfo requestPathInfo) {
+        return create()
+                .setResourcePath(requestPathInfo.getResourcePath())
+                .setSelectors(requestPathInfo.getSelectors())
+                .setExtension(requestPathInfo.getExtension())
+                .setSuffix(requestPathInfo.getSuffix());
+    }
+
+    /** Creates a builder from a request.
+     * 
+     * @param request
+     * @return a ResourceUriBuilder */
+    public static ResourceUriBuilder createFrom(SlingHttpServletRequest request) {
+        return createFrom(request.getRequestPathInfo())
+                .setResourceResolver(request.getResourceResolver())
+                .setScheme(request.getScheme())
+                .setHost(request.getServerName())
+                .setPort(request.getServerPort());
+    }
+
+    /** Creates a builder from a URI.
+     * 
+     * @param uri
+     * @return a ResourceUriBuilder */
+    public static ResourceUriBuilder createFrom(URI uri, ResourceResolver resourceResolver) {
+        String path = uri.getPath();
+        boolean pathExists = !StringUtils.isBlank(path);
+        boolean schemeSpecificRelevant = !pathExists && uri.getQuery() == null;
+        return create()
+                .setResourceResolver(resourceResolver)
+                .setScheme(uri.getScheme())
+                .setUserInfo(uri.getUserInfo())
+                .setHost(uri.getHost())
+                .setPort(uri.getPort())
+                .setPath(pathExists ? path : null)
+                .setQuery(uri.getQuery())
+                .setFragment(uri.getFragment())
+                .setSchemeSpecificPart(schemeSpecificRelevant ? uri.getSchemeSpecificPart() : null);
+    }
+
+    /** Creates a builder from an arbitrary URI string.
+     * 
+     * @param resourceUriStr
+     * @return a ResourceUriBuilder */
+    public static ResourceUriBuilder parse(String resourceUriStr, ResourceResolver resourceResolver) {
+        URI uri;
+        try {
+            uri = new URI(resourceUriStr);
+            return createFrom(uri, resourceResolver);
+        } catch (URISyntaxException e) {
+            throw new IllegalArgumentException("Invalid URI " + resourceUriStr + ": " + e.getMessage(), e);
+        }
+    }
+
+    /** Creates a builder from a resource path.
+     * 
+     * @param resourcePathStr
+     * @return a ResourceUriBuilder */
+    public static ResourceUriBuilder forPath(String resourcePathStr) {
+        return new ResourceUriBuilder().setPath(resourcePathStr);
+    }
+
+    private String scheme = null;
+
+    private String userInfo = null;
+    private String host = null;
+    private int port = -1;
+
+    private String resourcePath = null;
+    private final List<String> selectors = new LinkedList<>();
+    private String extension = null;
+    private final Map<String, String> pathParameters = new LinkedHashMap<>();
+    private String suffix = null;
+    private String schemeSpecificPart = null;
+    private String query = null;
+    private String fragment = null;
+
+    // only needed for getSuffixResource() from interface RequestPathInfo
+    private ResourceResolver resourceResolver = null;
+
+    private ResourceUriBuilder() {
+    }
+
+    /** @param userInfo
+     * @return the builder for method chaining */
+    public ResourceUriBuilder setUserInfo(String userInfo) {
+        if (schemeSpecificPart != null) {
+            return this;
+        }
+        this.userInfo = userInfo;
+        return this;
+    }
+
+    /** @param host
+     * @return the builder for method chaining */
+    public ResourceUriBuilder setHost(String host) {
+        if (schemeSpecificPart != null) {
+            return this;
+        }
+        this.host = host;
+        return this;
+    }
+
+    /** @param port
+     * @return the builder for method chaining */
+    public ResourceUriBuilder setPort(int port) {
+        if (schemeSpecificPart != null) {
+            return this;
+        }
+        this.port = port;
+        return this;
+    }
+
+    /** @param path
+     * @return the builder for method chaining */
+    public ResourceUriBuilder setPath(String path) {
+        if (schemeSpecificPart != null) {
+            return this;
+        }
+
+        // path parameters
+        Map<String, String> currentPathParameters = null;
+        if (path != null) {
+            Pattern pathParameterRegex = Pattern.compile(";([a-zA-z0-9]+)=(?:\\'([^']*)\\'|([^/]+))");
+
+            StringBuffer resultString = null;
+            Matcher regexMatcher = pathParameterRegex.matcher(path);
+            while (regexMatcher.find()) {
+                if (resultString == null) {
+                    resultString = new StringBuffer();
+                }
+                if (currentPathParameters == null) {
+                    currentPathParameters = new LinkedHashMap<>();
+                }
+                regexMatcher.appendReplacement(resultString, "");
+                String key = regexMatcher.group(1);
+                String value = StringUtils.defaultIfEmpty(regexMatcher.group(2), regexMatcher.group(3));
+                currentPathParameters.put(key, value);
+            }
+            if (resultString != null) {
+                regexMatcher.appendTail(resultString);
+                path = resultString.toString();
+                pathParameters.putAll(currentPathParameters);
+            }
+        }
+
+        // regular RequestPathInfo
+        Matcher dotMatcher;
+        if (path != null && (dotMatcher = Pattern.compile(SELECTOR_DOT_REGEX).matcher(path)).find()) {
+            int firstDotPosition = dotMatcher.start();
+            int firstSlashAfterFirstDotPosition = path.indexOf(CHAR_SLASH, firstDotPosition);
+            String pathWithoutSuffix = firstSlashAfterFirstDotPosition > -1 ? path.substring(0, firstSlashAfterFirstDotPosition) : path;
+            String[] pathBits = pathWithoutSuffix.split(SELECTOR_DOT_REGEX);
+            setResourcePath(pathBits[0]);
+            if (pathBits.length > 2) {
+                setSelectors(Arrays.copyOfRange(pathBits, 1, pathBits.length - 1));
+            }
+            setExtension(pathBits.length > 1 ? pathBits[pathBits.length - 1] : null);
+            setSuffix(firstSlashAfterFirstDotPosition > -1 ? path.substring(firstSlashAfterFirstDotPosition) : null);
+        } else {
+            setResourcePath(path);
+        }
+
+        if (resourceResolver != null) {
+            balanceResourcePath();
+        }
+
+        return this;
+    }
+
+    public ResourceUriBuilder balanceResourcePath() {
+        if (schemeSpecificPart != null) {
+            return this;
+        }
+        if (resourceResolver == null) {
+            throw new IllegalStateException("setResourceResolver() needs to be called before balanceResourcePath()");
+        }
+        List<String> potentialResourcePathBits = new ArrayList<>();
+        potentialResourcePathBits.add(resourcePath);
+        potentialResourcePathBits.addAll(selectors);
+        if (extension != null) {
+            potentialResourcePathBits.add(extension);
+        }
+        String fullPathWithSuffix = String.join(".", potentialResourcePathBits) + suffix;
+        if (resourceResolver.getResource(fullPathWithSuffix) != null) {
+            this.resourcePath = fullPathWithSuffix;
+            selectors.clear();
+            extension = null;
+            suffix = null;
+        } else {
+            for (int i = potentialResourcePathBits.size(); i > 1; i--) {
+                String potentialResourcePath = String.join(".", potentialResourcePathBits.subList(0, i));
+                if (resourceResolver.getResource(potentialResourcePath) != null) {
+                    this.resourcePath = potentialResourcePath;
+                    selectors.clear();
+                    extension = null;
+                    List<String> remainingList = potentialResourcePathBits.subList(i, potentialResourcePathBits.size());
+                    if (!remainingList.isEmpty()) {
+                        extension = remainingList.get(remainingList.size() - 1);
+                        selectors.addAll(remainingList.subList(0, remainingList.size() - 1));
+                    }
+                    break;
+                }
+            }
+        }
+        return this;
+    }
+
+    /** @param resourcePath
+     * @return the builder for method chaining */
+    public ResourceUriBuilder setResourcePath(String resourcePath) {
+        if (schemeSpecificPart != null) {
+            return this;
+        }
+        this.resourcePath = resourcePath;
+        return this;
+    }
+
+    /** @param selectors
+     * @return the builder for method chaining */
+    public ResourceUriBuilder setSelectors(String[] selectors) {
+        if (schemeSpecificPart != null || resourcePath == null) {
+            return this;
+        }
+        this.selectors.clear();
+        Arrays.stream(selectors).forEach(this.selectors::add);
+        return this;
+    }
+
+    /** @param selector
+     * @return the builder for method chaining */
+    public ResourceUriBuilder addSelector(String selector) {
+        if (schemeSpecificPart != null || resourcePath == null) {
+            return this;
+        }
+        this.selectors.add(selector);
+        return this;
+    }
+
+    /** @param extension
+     * @return the builder for method chaining */
+    public ResourceUriBuilder setExtension(String extension) {
+        if (schemeSpecificPart != null || resourcePath == null) {
+            return this;
+        }
+        this.extension = extension;
+        return this;
+    }
+
+    /** @return returns the path parameters */
+    public ResourceUriBuilder setPathParameter(String key, String value) {
+        if (schemeSpecificPart != null || resourcePath == null) {
+            return this;
+        }
+        this.pathParameters.put(key, value);
+        return this;
+    }
+
+    public ResourceUriBuilder setPathParameters(Map<String, String> pathParameters) {
+        this.pathParameters.clear();
+        this.pathParameters.putAll(pathParameters);
+        return this;
+    }
+
+    /** @param suffix
+     * @return the builder for method chaining */
+    public ResourceUriBuilder setSuffix(String suffix) {
+        if (schemeSpecificPart != null || resourcePath == null) {
+            return this;
+        }
+        if (suffix != null && !StringUtils.startsWith(suffix, "/")) {
+            throw new IllegalArgumentException("Suffix needs to start with slash");
+        }
+        this.suffix = suffix;
+        return this;
+    }
+
+    /** @param query
+     * @return the builder for method chaining */
+    public ResourceUriBuilder setQuery(String query) {
+        if (schemeSpecificPart != null) {
+            return this;
+        }
+        this.query = query;
+        return this;
+    }
+
+    /** @param urlFragment
+     * @return the builder for method chaining */
+    public ResourceUriBuilder setFragment(String urlFragment) {
+        if (schemeSpecificPart != null) {
+            return this;
+        }
+        this.fragment = urlFragment;
+        return this;
+    }
+
+    /** @param scheme
+     * @return the builder for method chaining */
+    public ResourceUriBuilder setScheme(String scheme) {
+        this.scheme = scheme;
+        return this;
+    }
+
+    /** @param schemeSpecificPart
+     * @return the builder for method chaining */
+    public ResourceUriBuilder setSchemeSpecificPart(String schemeSpecificPart) {
+        if (schemeSpecificPart != null && schemeSpecificPart.isEmpty()) {
+            return this;
+        }
+        this.schemeSpecificPart = schemeSpecificPart;
+        return this;
+    }
+
+    /** Will remove scheme and authority (that is user info, host and port).
+     * 
+     * @return the builder for method chaining */
+    public ResourceUriBuilder removeSchemeAndAuthority() {
+        setScheme(null);
+        setUserInfo(null);
+        setHost(null);
+        setPort(-1);
+        return this;
+    }
+
+    /** Will take over scheme and authority (user info, host and port) from provided resourceUri.
+     * 
+     * @param resourceUri
+     * @return the builder for method chaining */
+    public ResourceUriBuilder useSchemeAndAuthority(ResourceUri resourceUri) {
+        setScheme(resourceUri.getScheme());
+        setUserInfo(resourceUri.getUserInfo());
+        setHost(resourceUri.getHost());
+        setPort(resourceUri.getPort());
+        return this;
+    }
+
+    // only to support getSuffixResource() from interface RequestPathInfo
+    private ResourceUriBuilder setResourceResolver(ResourceResolver resourceResolver) {
+        this.resourceResolver = resourceResolver;
+        return this;
+    }
+
+    /** Will take over scheme and authority (user info, host and port) from provided uri.
+     * 
+     * @param uri
+     * @return the builder for method chaining */
+    public ResourceUriBuilder useSchemeAndAuthority(URI uri) {
+        useSchemeAndAuthority(createFrom(uri, resourceResolver).build());
+        return this;
+    }
+
+    /** Builds the immutable ResourceUri from this builder.
+     * 
+     * @return the builder for method chaining */
+    public ResourceUri build() {
+        return new ImmutableResourceUri();
+    }
+
+    /** @return string representation of builder */
+    public String toString() {
+        return build().toString();
+    }
+
+    // read-only view on the builder data (to avoid another copy of the data into a new object)
+    private class ImmutableResourceUri implements ResourceUri {
+
+        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;
+
+        @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 pathParameters;
+        }
+
+        @Override
+        public String getSuffix() {
+            return suffix;
+        }
+
+        @Override
+        public String getSchemeSpecificPart() {
+            return schemeSpecificPart;
+        }
+
+        @Override
+        public String getQuery() {
+            return query;
+        }
+
+        @Override
+        public String getFragment() {
+            return fragment;
+        }
+
+        @Override
+        public String getScheme() {
+            return scheme;
+        }
+
+        @Override
+        public String getHost() {
+            return host;
+        }
+
+        @Override
+        public int getPort() {
+            return port;
+        }
+
+        @Override
+        public Resource getSuffixResource() {
+            if (StringUtils.isNotBlank(suffix) && resourceResolver != null) {
+                return resourceResolver.resolve(suffix);
+            } else {
+                return null;
+            }
+        }
+
+        @Override
+        public String getUserInfo() {
+            return userInfo;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder requestUri = new StringBuilder();
+
+            if (StringUtils.isNotBlank(scheme)) {
+                requestUri.append(scheme + CHAR_COLON);
+            }
+            if (isFullUri()) {
+                requestUri.append(CHAR_SLASH + CHAR_SLASH);
+                if (StringUtils.isNotBlank(userInfo)) {
+                    requestUri.append(userInfo + CHAR_AT);
+                }
+                requestUri.append(host);
+                if (port > 0
+                        && !(scheme.equals(HTTP_SCHEME) && port == HTTP_DEFAULT_PORT)
+                        && !(scheme.equals(HTTPS_SCHEME) && port == HTTPS_DEFAULT_PORT)) {
+                    requestUri.append(CHAR_COLON + port);
+                }
+            }
+            if (resourcePath != null) {
+                requestUri.append(resourcePath);
+            }
+            if (!pathParameters.isEmpty()) {
+                for (Map.Entry<String, String> pathParameter : pathParameters.entrySet()) {
+                    requestUri.append(CHAR_SEMICOLON + pathParameter.getKey() + CHAR_EQUALS +
+                            CHAR_SINGLEQUOTE + pathParameter.getValue() + CHAR_SINGLEQUOTE);
+                }
+            }
+
+            if (!selectors.isEmpty()) {
+                requestUri.append(CHAR_DOT + String.join(CHAR_DOT, selectors));
+            }
+            if (!StringUtils.isBlank(extension)) {
+                requestUri.append(CHAR_DOT + extension);
+            }
+
+            if (!StringUtils.isBlank(suffix)) {
+                requestUri.append(suffix);
+            }
+            if (schemeSpecificPart != null) {
+                requestUri.append(schemeSpecificPart);
+            }
+            if (query != null) {
+                requestUri.append(CHAR_QM + query);
+            }
+            if (fragment != null) {
+                requestUri.append(CHAR_HASH + fragment);
+            }
+            return requestUri.toString();
+        }
+
+        @Override
+        public URI toUri() {
+            String uriString = toString();
+            try {
+                return new URI(uriString);
+            } catch (URISyntaxException e) {
+                throw new IllegalArgumentException("Invalid Sling URI: " + uriString, e);
+            }
+        }
+
+        private ResourceUriBuilder getBuilder() {
+            return ResourceUriBuilder.this;
+        }
+
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/api/resource/package-info.java b/src/main/java/org/apache/sling/api/resource/uri/package-info.java
similarity index 93%
copy from src/main/java/org/apache/sling/api/resource/package-info.java
copy to src/main/java/org/apache/sling/api/resource/uri/package-info.java
index 7bd85e6..dd21e49 100644
--- a/src/main/java/org/apache/sling/api/resource/package-info.java
+++ b/src/main/java/org/apache/sling/api/resource/uri/package-info.java
@@ -16,9 +16,8 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
-@Version("2.12.2")
-package org.apache.sling.api.resource;
+@Version("1.0.0")
+package org.apache.sling.api.resource.uri;
 
 import org.osgi.annotation.versioning.Version;
 
diff --git a/src/main/java/org/apache/sling/spi/resource/mapping/MappingChainContext.java b/src/main/java/org/apache/sling/spi/resource/mapping/MappingChainContext.java
new file mode 100644
index 0000000..cf6d541
--- /dev/null
+++ b/src/main/java/org/apache/sling/spi/resource/mapping/MappingChainContext.java
@@ -0,0 +1,51 @@
+/*
+ * 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.spi.resource.mapping;
+
+import java.util.Map;
+
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.mapping.PathToUriMappingService.ContextHint;
+import org.apache.sling.api.resource.uri.ResourceUri;
+import org.osgi.annotation.versioning.ProviderType;
+
+/** Provides ResourceToUriMapper instances with additional context. */
+@ProviderType
+public interface MappingChainContext {
+
+    /** May be called by any ResourceUriMapper in the chain to indicate that the rest of the chain should be skipped. */
+    void skipRemainingChain();
+
+    /** Add @{link ContextHint} (e.g. 'invalid link' for map(), or 'requires authentication' for resolve()) */
+    void addContextHint(ContextHint contextHint);
+
+    /** The resource resolver that was used to call map() or resolve(). */
+    ResourceResolver getResourceResolver();
+
+    /** Allows to share state between ResourceToUriMapper instances in the chain.
+     * 
+     * @return a mutable map to share state (never null). */
+    Map<String, Object> getAttributes();
+
+    /** Provides access to intermediate mappings.
+     * 
+     * @return the resource mappings */
+    Map<String, ResourceUri> getIntermediateMappings();
+
+}
diff --git a/src/main/java/org/apache/sling/spi/resource/mapping/ResourceUriMapper.java b/src/main/java/org/apache/sling/spi/resource/mapping/ResourceUriMapper.java
new file mode 100644
index 0000000..d3fed13
--- /dev/null
+++ b/src/main/java/org/apache/sling/spi/resource/mapping/ResourceUriMapper.java
@@ -0,0 +1,56 @@
+/*
+ * 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.spi.resource.mapping;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.sling.api.resource.uri.ResourceUri;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.osgi.annotation.versioning.ConsumerType;
+
+/** SPI interface that contributes to resource mapping and resolving of the resource resolver's map() and resolve() methods.
+ * 
+ * All registered services build a conceptual chain sorted by service ranking. The resource URI is passed through the chain while any
+ * ResourceUriMapper chain member may or may not make adjustments to the resource URI.
+ * 
+ * rr.resolve() passes through the chain starting at the ResourceUriMapper with the <strong>highest</strong> service ranking and rr.map()
+ * passes through the chain starting at the ResourceUriMapper with the <strong>lowest</strong> service ranking */
+@ConsumerType
+public interface ResourceUriMapper {
+
+    /** Contributes to the map process, may or may not make adjustments to the resource URI.
+     * 
+     * @param resourceUri the URI to be mapped
+     * @param request the request to be taken as example
+     * @param context can be used to skip further processing of the chain or for sharing state between instances of ResourceUriMapper
+     *            services
+     * @return the adjusted ResourceUri or if no adjustments are necessary, just return resourceUri as passed in by first parameter */
+    ResourceUri map(@NotNull ResourceUri resourceUri, @Nullable HttpServletRequest request, @NotNull MappingChainContext context);
+
+    /** Contributes to the resolve process, may or may not make adjustments to the resource URI
+     * 
+     * @param resourceUri the URI to be resolved
+     * @param request the request context that may or may not influence the resolution process (request may be null)
+     * @param context can be used to skip further processing of the chain or for sharing state between instances of ResourceUriMapper
+     *            services
+     * @return the adjusted ResourceUri or if no adjustments are necessary, just return resourceUri as passed in by first parameter */
+    ResourceUri resolve(@NotNull ResourceUri resourceUri, @Nullable HttpServletRequest request, @NotNull MappingChainContext context);
+
+}
diff --git a/src/main/java/org/apache/sling/api/resource/mapping/package-info.java b/src/main/java/org/apache/sling/spi/resource/mapping/package-info.java
similarity index 92%
copy from src/main/java/org/apache/sling/api/resource/mapping/package-info.java
copy to src/main/java/org/apache/sling/spi/resource/mapping/package-info.java
index e871225..c8c07af 100644
--- a/src/main/java/org/apache/sling/api/resource/mapping/package-info.java
+++ b/src/main/java/org/apache/sling/spi/resource/mapping/package-info.java
@@ -17,8 +17,8 @@
  * under the License.
  */
 
-@Version("1.0.1")
-package org.apache.sling.api.resource.mapping;
+@Version("1.0.0")
+package org.apache.sling.spi.resource.mapping;
 
 import org.osgi.annotation.versioning.Version;
 
diff --git a/src/test/java/org/apache/sling/api/resource/uri/ResourceUriTest.java b/src/test/java/org/apache/sling/api/resource/uri/ResourceUriTest.java
new file mode 100644
index 0000000..3711631
--- /dev/null
+++ b/src/test/java/org/apache/sling/api/resource/uri/ResourceUriTest.java
@@ -0,0 +1,679 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.api.resource.uri;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.when;
+
+import java.util.function.Consumer;
+
+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 ResourceUriTest {
+
+    @Mock
+    ResourceResolver resourceResolver;
+
+    @Mock
+    Resource resource;
+
+    @Test
+    public void testFullResourceUri() {
+
+        String testUriStr = "http://host.com/test/to/path.html";
+        testUri(testUriStr, false, false, false, true, resourceUri -> {
+            assertEquals("http", resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals("host.com", resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals("/test/to/path", resourceUri.getResourcePath());
+            assertEquals(null, resourceUri.getSelectorString());
+            assertEquals("html", resourceUri.getExtension());
+            assertEquals(null, resourceUri.getSuffix());
+            assertEquals(null, resourceUri.getSchemeSpecificPart());
+            assertEquals(null, resourceUri.getQuery());
+            assertEquals(null, resourceUri.getFragment());
+        });
+
+    }
+
+    @Test
+    public void testFullResourceUriComplex() {
+
+        String testUriStr = "https://test:pw@host.com:888/test/to/path.sel1.json/suffix/path?p1=2&p2=3#frag3939";
+        testUri(testUriStr, false, false, false, true, resourceUri -> {
+            assertEquals("https", resourceUri.getScheme());
+            assertEquals("test:pw", resourceUri.getUserInfo());
+            assertEquals("host.com", resourceUri.getHost());
+            assertEquals(888, resourceUri.getPort());
+            assertEquals("/test/to/path", resourceUri.getResourcePath());
+            assertEquals("sel1", resourceUri.getSelectorString());
+            assertEquals("json", resourceUri.getExtension());
+            assertEquals("/suffix/path", resourceUri.getSuffix());
+            assertEquals(null, resourceUri.getSchemeSpecificPart());
+            assertEquals("p1=2&p2=3", resourceUri.getQuery());
+            assertEquals("frag3939", resourceUri.getFragment());
+        });
+
+    }
+
+    @Test
+    public void testAbsolutePathResourceUri() {
+        String testUriStr = "/test/to/path.sel1.json/suffix/path?p1=2&p2=3#frag3939";
+
+        testUri(testUriStr, true, true, false, false, resourceUri -> {
+            assertEquals(null, resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals(null, resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals("/test/to/path", resourceUri.getResourcePath());
+            assertEquals("sel1", resourceUri.getSelectorString());
+            assertEquals("json", resourceUri.getExtension());
+            assertEquals("/suffix/path", resourceUri.getSuffix());
+            assertEquals(null, resourceUri.getSchemeSpecificPart());
+            assertEquals("p1=2&p2=3", resourceUri.getQuery());
+            assertEquals("frag3939", resourceUri.getFragment());
+        });
+    }
+
+    @Test
+    public void testResourceUriSuffixWithDots() {
+
+        String testUriStr = "/test/to/path.min.js/suffix/app.nodesbrowser.js";
+        testUri(testUriStr, true, true, false, false, resourceUri -> {
+            assertEquals(null, resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals(null, resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals("/test/to/path", resourceUri.getResourcePath());
+            assertEquals("min", resourceUri.getSelectorString());
+            assertEquals("js", resourceUri.getExtension());
+            assertEquals("/suffix/app.nodesbrowser.js", resourceUri.getSuffix());
+            assertEquals(null, resourceUri.getSchemeSpecificPart());
+            assertEquals(null, resourceUri.getQuery());
+            assertEquals(null, resourceUri.getFragment());
+        });
+    }
+
+    @Test
+    public void testResourceUriMultipleDots() {
+
+        String testUriStr = "/test/to/path.sel1.sel2..sel4.js";
+        testUri(testUriStr, true, true, false, false, resourceUri -> {
+            assertEquals(null, resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals(null, resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals("/test/to/path", resourceUri.getResourcePath());
+            assertEquals(4, resourceUri.getSelectors().length);
+            assertEquals("sel1.sel2..sel4", resourceUri.getSelectorString());
+            assertEquals("js", resourceUri.getExtension());
+            assertEquals(null, resourceUri.getSuffix());
+            assertEquals(null, resourceUri.getSchemeSpecificPart());
+            assertEquals(null, resourceUri.getQuery());
+            assertEquals(null, resourceUri.getFragment());
+        });
+        
+        String testUriStr2 = "/test/to/path.sel1.sel2../sel4.js";
+        testUri(testUriStr2, true, true, false, false, resourceUri -> {
+            assertEquals(null, resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals(null, resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals("/test/to/path", resourceUri.getResourcePath());
+            assertEquals(1, resourceUri.getSelectors().length);
+            assertEquals("sel1", resourceUri.getSelectorString());
+            assertEquals("sel2", resourceUri.getExtension());
+            assertEquals("/sel4.js", resourceUri.getSuffix());
+            assertEquals(null, resourceUri.getSchemeSpecificPart());
+            assertEquals(null, resourceUri.getQuery());
+            assertEquals(null, resourceUri.getFragment());
+        }, true);
+    }
+
+    @Test
+    public void testRelativePathResourceUri() {
+        String testUriStr = "../path.html#frag1";
+
+        testUri(testUriStr, true, false, true, false, resourceUri -> {
+            assertEquals(null, resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals(null, resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals("../path", resourceUri.getResourcePath());
+            assertEquals(null, resourceUri.getSelectorString());
+            assertEquals("html", resourceUri.getExtension());
+            assertEquals(null, resourceUri.getSuffix());
+            assertEquals(null, resourceUri.getSchemeSpecificPart());
+            assertEquals(null, resourceUri.getQuery());
+            assertEquals("frag1", resourceUri.getFragment());
+        });
+    }
+
+    @Test
+    public void testRelativePathResourceUriComplex() {
+        String testUriStr = "../path/./deep/path/../path.sel1.sel2.html?test=1#frag1";
+
+        testUri(testUriStr, true, false, true, false, resourceUri -> {
+            assertEquals(null, resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals(null, resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals("../path/./deep/path/../path", resourceUri.getResourcePath());
+            assertEquals("sel1.sel2", resourceUri.getSelectorString());
+            assertEquals("html", resourceUri.getExtension());
+            assertEquals(null, resourceUri.getSuffix());
+            assertEquals(null, resourceUri.getSchemeSpecificPart());
+            assertEquals("test=1", resourceUri.getQuery());
+            assertEquals("frag1", resourceUri.getFragment());
+        });
+    }
+
+    @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, resourceUri -> {
+            assertEquals(null, resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals(null, resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals("/test/to/path", resourceUri.getResourcePath());
+            assertEquals("sel1", resourceUri.getSelectorString());
+            assertEquals("html", resourceUri.getExtension());
+            assertEquals(1, resourceUri.getPathParameters().size());
+            assertEquals("1.0", resourceUri.getPathParameters().get("v"));
+            assertEquals("/suffix/path", resourceUri.getSuffix());
+            assertEquals(null, resourceUri.getSchemeSpecificPart());
+            assertEquals("p1=2&p2=3", resourceUri.getQuery());
+            assertEquals("frag3939", resourceUri.getFragment());
+        });
+
+        String testUriStr2 = "/test/to/file;foo='bar'.sel1.sel2.json/suffix/path";
+        testUri(testUriStr2, true, true, false, false, resourceUri -> {
+            assertEquals(null, resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals(null, resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals("/test/to/file", resourceUri.getResourcePath());
+            assertEquals("sel1.sel2", resourceUri.getSelectorString());
+            assertEquals("json", resourceUri.getExtension());
+            assertEquals(1, resourceUri.getPathParameters().size());
+            assertEquals("bar", resourceUri.getPathParameters().get("foo"));
+            assertEquals("/suffix/path", resourceUri.getSuffix());
+            assertEquals(null, resourceUri.getSchemeSpecificPart());
+            assertEquals(null, resourceUri.getQuery());
+            assertEquals(null, resourceUri.getFragment());
+        });
+    }
+
+    @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, resourceUri -> {
+            assertEquals(null, resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals(null, resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals("/test/to/path", resourceUri.getResourcePath());
+            assertEquals("sel1", resourceUri.getSelectorString());
+            assertEquals("html", resourceUri.getExtension());
+
+            assertEquals(3, resourceUri.getPathParameters().size());
+            assertEquals("1.0", resourceUri.getPathParameters().get("v"));
+            assertEquals("test/nested", resourceUri.getPathParameters().get("antotherParam"));
+            assertEquals("7", resourceUri.getPathParameters().get("antotherParam2"));
+
+            assertEquals("/suffix/path", resourceUri.getSuffix());
+            assertEquals(null, resourceUri.getSchemeSpecificPart());
+            assertEquals("p1=2&p2=3", resourceUri.getQuery());
+            assertEquals("frag3939", resourceUri.getFragment());
+        });
+    }
+
+    @Test
+    public void testAbsolutePathWithPathParameterAfterExtension() {
+        String testUriStr = "/test/to/path.sel1.html;v='1.0'/suffix/path?p1=2&p2=3#frag3939";
+
+        ResourceUri testUri = testUri(testUriStr, true, true, false, false, resourceUri -> {
+            assertEquals(null, resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals(null, resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals("/test/to/path", resourceUri.getResourcePath());
+            assertEquals("sel1", resourceUri.getSelectorString());
+            assertEquals("html", resourceUri.getExtension());
+            assertEquals(1, resourceUri.getPathParameters().size());
+            assertEquals("1.0", resourceUri.getPathParameters().get("v"));
+            assertEquals("/suffix/path", resourceUri.getSuffix());
+            assertEquals(null, resourceUri.getSchemeSpecificPart());
+            assertEquals("p1=2&p2=3", resourceUri.getQuery());
+            assertEquals("frag3939", resourceUri.getFragment());
+        }, true /* URL is restructured (parameter moved to end), assertion below */);
+
+        assertEquals("/test/to/path;v='1.0'.sel1.html/suffix/path?p1=2&p2=3#frag3939", testUri.toString());
+
+    }
+
+    @Test
+    public void testJavascriptUri() {
+        String testUriStr = "javascript:void(0)";
+
+        testUri(testUriStr, false, false, false, false, resourceUri -> {
+            assertEquals("javascript", resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals(null, resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals(null, resourceUri.getSelectorString());
+            assertEquals(null, resourceUri.getExtension());
+            assertEquals(null, resourceUri.getSuffix());
+            assertEquals("void(0)", resourceUri.getSchemeSpecificPart());
+            assertEquals(null, resourceUri.getQuery());
+            assertEquals(null, resourceUri.getFragment());
+        });
+    }
+
+    @Test
+    public void testMailtotUri() {
+        String testUriStr = "mailto:jon.doe@example.com";
+
+        testUri(testUriStr, false, false, false, false, resourceUri -> {
+            assertEquals("mailto", resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals(null, resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals(null, resourceUri.getSelectorString());
+            assertEquals(null, resourceUri.getExtension());
+            assertEquals(null, resourceUri.getSuffix());
+            assertEquals("jon.doe@example.com", resourceUri.getSchemeSpecificPart());
+            assertEquals(null, resourceUri.getQuery());
+            assertEquals(null, resourceUri.getFragment());
+        });
+    }
+
+    @Test
+    public void testHashOnlyUri() {
+
+        testUri("#", false, false, false, false, resourceUri -> {
+            assertEquals(null, resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals(null, resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals(null, resourceUri.getResourcePath());
+            assertEquals(null, resourceUri.getSelectorString());
+            assertEquals(null, resourceUri.getExtension());
+            assertEquals(null, resourceUri.getSuffix());
+            assertEquals(null, resourceUri.getSchemeSpecificPart());
+            assertEquals(null, resourceUri.getQuery());
+            assertEquals("", resourceUri.getFragment());
+        });
+
+        testUri("#fragment", false, false, false, false, resourceUri -> {
+            assertEquals(null, resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals(null, resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals(null, resourceUri.getResourcePath());
+            assertEquals(null, resourceUri.getSelectorString());
+            assertEquals(null, resourceUri.getExtension());
+            assertEquals(null, resourceUri.getSuffix());
+            assertEquals(null, resourceUri.getSchemeSpecificPart());
+            assertEquals(null, resourceUri.getQuery());
+            assertEquals("fragment", resourceUri.getFragment());
+        });
+    }
+
+    @Test
+    public void testQueryOnlyUri() {
+
+        testUri("?", false, false, false, false, resourceUri -> {
+            assertEquals(null, resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals(null, resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals(null, resourceUri.getResourcePath());
+            assertEquals(null, resourceUri.getSelectorString());
+            assertEquals(null, resourceUri.getExtension());
+            assertEquals(null, resourceUri.getSuffix());
+            assertEquals(null, resourceUri.getSchemeSpecificPart());
+            assertEquals("", resourceUri.getQuery());
+            assertEquals(null, resourceUri.getFragment());
+        });
+
+        testUri("?test=test", false, false, false, false, resourceUri -> {
+            assertEquals(null, resourceUri.getScheme());
+            assertEquals(null, resourceUri.getUserInfo());
+            assertEquals(null, resourceUri.getHost());
+            assertEquals(-1, resourceUri.getPort());
+            assertEquals(null, resourceUri.getResourcePath());
+            assertEquals(null, resourceUri.getSelectorString());
+            assertEquals(null, resourceUri.getExtension());
+            assertEquals(null, resourceUri.getSuffix());
+            assertEquals(null, resourceUri.getSchemeSpecificPart());
+            assertEquals("test=test", resourceUri.getQuery());
+            assertEquals(null, resourceUri.getFragment());
+        });
+    }
+
+    @Test
+    public void testBalanceResourcePathSimpleCases() {
+        // simple case
+        String testUriStrSimple = "/test/to/file";
+        when(resourceResolver.getResource("/test/to/file")).thenReturn(resource);
+        testUri(testUriStrSimple, true, true, false, false, resourceUri -> {
+            assertEquals("/test/to/file", resourceUri.getResourcePath());
+            assertEquals(null, resourceUri.getSelectorString());
+            assertEquals(null, resourceUri.getExtension());
+            assertEquals(null, resourceUri.getSuffix());
+        });
+
+        // simple file case
+        String testUriStrSimpleFile = "/test/to/file.css";
+        when(resourceResolver.getResource("/test/to/file.css")).thenReturn(resource);
+        when(resourceResolver.getResource("/test/to/file")).thenReturn(null);
+        testUri(testUriStrSimpleFile, true, true, false, false, resourceUri -> {
+            assertEquals("/test/to/file.css", resourceUri.getResourcePath());
+            assertEquals(null, resourceUri.getSelectorString());
+            assertEquals(null, resourceUri.getExtension());
+            assertEquals(null, resourceUri.getSuffix());
+        });
+
+        // simple html rendering case
+        String testUriStrSimplePage = "/path/to/page.html";
+        when(resourceResolver.getResource("/path/to/page.html")).thenReturn(null);
+        when(resourceResolver.getResource("/path/to/page")).thenReturn(resource);
+        testUri(testUriStrSimplePage, true, true, false, false, resourceUri -> {
+            assertEquals("/path/to/page", resourceUri.getResourcePath());
+            assertEquals(null, resourceUri.getSelectorString());
+            assertEquals("html", resourceUri.getExtension());
+            assertEquals(null, resourceUri.getSuffix());
+        });
+    }
+
+    @Test
+    public void testBalanceResourcePathWithSelectorsAndExtension() {
+
+        String testUriStr = "/test/to/file.ext.sel1.json/suffix/path.js";
+
+        // pull path with suffix is resource path
+        when(resourceResolver.getResource("/test/to/file.ext.sel1.json/suffix/path.js")).thenReturn(resource);
+        when(resourceResolver.getResource("/test/to/file.ext.sel1.json")).thenReturn(null);
+        when(resourceResolver.getResource("/test/to/file.ext.sel1")).thenReturn(null);
+        when(resourceResolver.getResource("/test/to/file.ext")).thenReturn(null);
+        when(resourceResolver.getResource("/test/to/file")).thenReturn(null);
+        testUri(testUriStr, true, true, false, false, resourceUri -> {
+            assertEquals("/test/to/file.ext.sel1.json/suffix/path.js", resourceUri.getResourcePath());
+            assertEquals(null, resourceUri.getSelectorString());
+            assertEquals(null, resourceUri.getExtension());
+            assertEquals(null, resourceUri.getSuffix());
+        });
+
+        // full path without suffix is resource path
+        when(resourceResolver.getResource("/test/to/file.ext.sel1.json/suffix/path.js")).thenReturn(null);
+        when(resourceResolver.getResource("/test/to/file.ext.sel1.json")).thenReturn(resource);
+        when(resourceResolver.getResource("/test/to/file.ext.sel1")).thenReturn(null);
+        when(resourceResolver.getResource("/test/to/file.ext")).thenReturn(null);
+        when(resourceResolver.getResource("/test/to/file")).thenReturn(null);
+        testUri(testUriStr, true, true, false, false, resourceUri -> {
+            assertEquals("/test/to/file.ext.sel1.json", resourceUri.getResourcePath());
+            assertEquals(null, resourceUri.getSelectorString());
+            assertEquals(null, resourceUri.getExtension());
+            assertEquals("/suffix/path.js", resourceUri.getSuffix());
+        });
+
+        // mix of extension and resource path with dots
+        when(resourceResolver.getResource("/test/to/file.ext.sel1.json/suffix/path.js")).thenReturn(null);
+        when(resourceResolver.getResource("/test/to/file.ext.sel1.json")).thenReturn(null);
+        when(resourceResolver.getResource("/test/to/file.ext.sel1")).thenReturn(resource);
+        when(resourceResolver.getResource("/test/to/file.ext")).thenReturn(null);
+        when(resourceResolver.getResource("/test/to/file")).thenReturn(null);
+        testUri(testUriStr, true, true, false, false, resourceUri -> {
+            assertEquals("/test/to/file.ext.sel1", resourceUri.getResourcePath());
+            assertEquals(null, resourceUri.getSelectorString());
+            assertEquals("json", resourceUri.getExtension());
+            assertEquals("/suffix/path.js", resourceUri.getSuffix());
+        });
+
+        when(resourceResolver.getResource("/test/to/file.ext.sel1.json/suffix/path.js")).thenReturn(null);
+        when(resourceResolver.getResource("/test/to/file.ext.sel1.json")).thenReturn(null);
+        when(resourceResolver.getResource("/test/to/file.ext.sel1")).thenReturn(null);
+        when(resourceResolver.getResource("/test/to/file.ext")).thenReturn(resource);
+        when(resourceResolver.getResource("/test/to/file")).thenReturn(null);
+        testUri(testUriStr, true, true, false, false, resourceUri -> {
+            assertEquals("/test/to/file.ext", resourceUri.getResourcePath());
+            assertEquals("sel1", resourceUri.getSelectorString());
+            assertEquals("json", resourceUri.getExtension());
+            assertEquals("/suffix/path.js", resourceUri.getSuffix());
+        });
+
+        // usual case: resource path does not contain dot
+        when(resourceResolver.getResource("/test/to/file.ext.sel1.json/suffix/path.js")).thenReturn(null);
+        when(resourceResolver.getResource("/test/to/file.ext.sel1.json")).thenReturn(null);
+        when(resourceResolver.getResource("/test/to/file.ext.sel1")).thenReturn(null);
+        when(resourceResolver.getResource("/test/to/file.ext")).thenReturn(null);
+        when(resourceResolver.getResource("/test/to/file")).thenReturn(resource);
+        testUri(testUriStr, true, true, false, false, resourceUri -> {
+            assertEquals("/test/to/file", resourceUri.getResourcePath());
+            assertEquals("ext.sel1", resourceUri.getSelectorString());
+            assertEquals("json", resourceUri.getExtension());
+            assertEquals("/suffix/path.js", resourceUri.getSuffix());
+        });
+
+        // side by side resources in same folder: the longest path wins
+        when(resourceResolver.getResource("/test/to/file.ext.sel1.json/suffix/path.js")).thenReturn(null);
+        when(resourceResolver.getResource("/test/to/file.ext.sel1.json")).thenReturn(null);
+        when(resourceResolver.getResource("/test/to/file.ext.sel1")).thenReturn(resource);
+        when(resourceResolver.getResource("/test/to/file.ext")).thenReturn(resource);
+        when(resourceResolver.getResource("/test/to/file")).thenReturn(resource);
+        testUri(testUriStr, true, true, false, false, resourceUri -> {
+            assertEquals("/test/to/file.ext.sel1", resourceUri.getResourcePath());
+            assertEquals(null, resourceUri.getSelectorString());
+            assertEquals("json", resourceUri.getExtension());
+            assertEquals("/suffix/path.js", resourceUri.getSuffix());
+        });
+    }
+
+    @Test
+    public void testUnusualQueryFragmentCombinations() {
+        testUri("?#", false, false, false, false, resourceUri -> {
+            assertEquals("", resourceUri.getQuery());
+            assertEquals("", resourceUri.getFragment());
+        });
+        testUri("?t=2#", false, false, false, false, resourceUri -> {
+            assertEquals("t=2", resourceUri.getQuery());
+            assertEquals("", resourceUri.getFragment());
+        });
+        testUri("?#t=3", false, false, false, false, resourceUri -> {
+            assertEquals("", resourceUri.getQuery());
+            assertEquals("t=3", resourceUri.getFragment());
+        });
+        testUri("", false, false, false, false, resourceUri -> {
+            assertEquals(null, resourceUri.getQuery());
+            assertEquals(null, resourceUri.getFragment());
+        });
+    }
+
+    // -- adjustment test cases
+
+    @Test
+    public void testAdjustAddSelectorFullUrl() {
+
+        testAdjustUri(
+                "http://host.com/test/to/path.html",
+                resourceUriBuilder -> {
+                    resourceUriBuilder.addSelector("test");
+                },
+                "http://host.com/test/to/path.test.html",
+                resourceUri -> {
+                    assertEquals("test", resourceUri.getSelectorString());
+                });
+    }
+
+    @Test
+    public void testAdjustAddSelectorAndSuffixPath() {
+
+        testAdjustUri(
+                "/test/to/path.html",
+                resourceUriBuilder -> {
+                    resourceUriBuilder.addSelector("test");
+                    resourceUriBuilder.setSuffix("/suffix/path/to/file");
+                },
+                "/test/to/path.test.html/suffix/path/to/file",
+                resourceUri -> {
+                    assertArrayEquals(new String[] { "test" }, resourceUri.getSelectors());
+                    assertEquals("/suffix/path/to/file", resourceUri.getSuffix());
+                });
+    }
+
+    @Test
+    public void testExtendSimplePathToFullUrl() {
+
+        testAdjustUri(
+                "/test/to/path.html",
+                resourceUriBuilder -> {
+                    resourceUriBuilder.setScheme("https");
+                    resourceUriBuilder.setHost("example.com");
+                    resourceUriBuilder.setSuffix("/suffix/path/to/file");
+                },
+                "https://example.com/test/to/path.html/suffix/path/to/file",
+                resourceUri -> {
+                    assertEquals("https", resourceUri.getScheme());
+                    assertEquals("example.com", resourceUri.getHost());
+                    assertEquals("/suffix/path/to/file", resourceUri.getSuffix());
+                });
+    }
+
+    @Test
+    public void testFullUrltoSimplePath() {
+
+        testAdjustUri(
+                "https://user:pw@example.com/test/to/path.html/suffix/path/to/file",
+                resourceUriBuilder -> {
+                    resourceUriBuilder.removeSchemeAndAuthority();
+                },
+                "/test/to/path.html/suffix/path/to/file",
+                resourceUri -> {
+                    assertEquals(null, resourceUri.getScheme());
+                    assertEquals(null, resourceUri.getUserInfo());
+                    assertEquals(null, resourceUri.getHost());
+                });
+    }
+
+    @Test
+    public void testAdjustPathInSpecialUriWithoutEffect() {
+
+        testAdjustUri(
+                "mailto:jon.doe@example.com",
+                resourceUriBuilder -> {
+                    resourceUriBuilder.setPath("/path/to/resource");
+                    resourceUriBuilder.setResourcePath("/path/to/resource");
+                    resourceUriBuilder.addSelector("test");
+                    resourceUriBuilder.setExtension("html");
+                    resourceUriBuilder.setSuffix("/suffix");
+                },
+                "mailto:jon.doe@example.com",
+                resourceUri -> {
+                    assertEquals(null, resourceUri.getResourcePath());
+                    assertEquals(null, resourceUri.getSelectorString());
+                    assertEquals(null, resourceUri.getExtension());
+                    assertEquals(null, resourceUri.getSuffix());
+                });
+    }
+
+    @Test
+    public void testAdjustSelectorsInFragmentOnlyUrlWithoutEffect() {
+
+        testAdjustUri(
+                "#fragment",
+                resourceUriBuilder -> {
+                    resourceUriBuilder.addSelector("test");
+                    resourceUriBuilder.setSuffix("/suffix");
+                },
+                "#fragment",
+                resourceUri -> {
+                    assertEquals(null, resourceUri.getSelectorString());
+                    assertEquals(null, resourceUri.getSuffix());
+                });
+    }
+
+    @Test
+    public void testAjustFtpUrl() {
+
+        testAdjustUri(
+                "sftp://user:pw@example.com:9090/some/path",
+                resourceUriBuilder -> {
+                    resourceUriBuilder.setPath("/some/other/path");
+                    resourceUriBuilder.setPort(9091);
+                },
+                "sftp://user:pw@example.com:9091/some/other/path",
+                resourceUri -> {
+                    assertEquals("/some/other/path", resourceUri.getResourcePath());
+                    assertEquals(null, resourceUri.getSelectorString());
+                    assertEquals(9091, resourceUri.getPort());
+                });
+    }
+
+    // -- helper methods
+
+
+    public ResourceUri testUri(String testUri, boolean isPath, boolean isAbsolutePath, boolean isRelativePath, boolean isFullUri,
+            Consumer<ResourceUri> additionalAssertions) {
+        return testUri(testUri, isPath, isAbsolutePath, isRelativePath, isFullUri, additionalAssertions, false);
+    }
+
+    public ResourceUri testUri(String testUri, boolean isPath, boolean isAbsolutePath, boolean isRelativePath, boolean isFullUri,
+            Consumer<ResourceUri> additionalAssertions, boolean urlIsRestructured) {
+        ResourceUri resourceUri = ResourceUriBuilder.parse(testUri, resourceResolver).build();
+
+        if (!urlIsRestructured) {
+            assertEquals(testUri, resourceUri.toString());
+            assertEquals(testUri, resourceUri.toUri().toString());
+        }
+
+        assertEquals("isPath()", isPath, resourceUri.isPath());
+        assertEquals("isAbsolutePath()", isAbsolutePath, resourceUri.isAbsolutePath());
+        assertEquals("isRelativePath()", isRelativePath, resourceUri.isRelativePath());
+        assertEquals("isFullUri()", isFullUri, resourceUri.isFullUri());
+
+        additionalAssertions.accept(resourceUri);
+
+        return resourceUri;
+    }
+
+    public void testAdjustUri(String testUri, Consumer<ResourceUriBuilder> adjuster, String testUriAfterEdit,
+            Consumer<ResourceUri> additionalAssertions) {
+        ResourceUri resourceUri = ResourceUriBuilder.parse(testUri, resourceResolver).build();
+
+        ResourceUri adjustedResourceUri = resourceUri.adjust(adjuster);
+
+        assertEquals(testUriAfterEdit, adjustedResourceUri.toString());
+        assertEquals(testUriAfterEdit, adjustedResourceUri.toUri().toString());
+
+        additionalAssertions.accept(adjustedResourceUri);
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/api/resource/uri/ResourceUriToSlingRequestPathInfoCompatibilityTest.java b/src/test/java/org/apache/sling/api/resource/uri/ResourceUriToSlingRequestPathInfoCompatibilityTest.java
new file mode 100644
index 0000000..b452be4
--- /dev/null
+++ b/src/test/java/org/apache/sling/api/resource/uri/ResourceUriToSlingRequestPathInfoCompatibilityTest.java
@@ -0,0 +1,271 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.api.resource.uri;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.when;
+
+import org.apache.sling.api.request.RequestPathInfo;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ResourceUriToSlingRequestPathInfoCompatibilityTest {
+
+    @Mock
+    ResourceResolver resourceResolver;
+
+    @Mock
+    Resource resource;
+
+    private RequestPathInfo createResourceUri(String resolutionPath, String resolutionPathInfo) {
+        when(resourceResolver.getResource(resolutionPath)).thenReturn(resource);
+        return ResourceUriBuilder.parse(resolutionPath + (resolutionPathInfo != null ? resolutionPathInfo : ""), resourceResolver).build();
+    }
+
+    @Test
+    public void testTrailingDot() {
+        RequestPathInfo p = createResourceUri("/some/path", ".");
+        assertEquals("/some/path", p.getResourcePath());
+        assertNull("Selectors are null", p.getSelectorString());
+        assertEquals(0, p.getSelectors().length);
+        assertNull("Extension is null", p.getExtension());
+        assertNull("Suffix is null", p.getSuffix());
+    }
+
+    @Test
+    @Ignore
+    public void testTrailingDotWithSuffix() {
+        RequestPathInfo p = createResourceUri("/some/path", "./suffix");
+        assertEquals("/some/path", p.getResourcePath());
+        assertNull("Selectors are null", p.getSelectorString());
+        assertEquals(0, p.getSelectors().length);
+        assertNull("Extension is null", p.getExtension());
+        assertEquals("/suffix", p.getSuffix());
+    }
+
+    @Test
+    public void testTrailingDotDot() {
+        RequestPathInfo p = createResourceUri("/some/path", "..");
+        assertEquals("/some/path", p.getResourcePath());
+        assertNull("Selectors are null", p.getSelectorString());
+        assertEquals(0, p.getSelectors().length);
+        assertNull("Extension is null", p.getExtension());
+        assertNull("Suffix is null", p.getSuffix());
+    }
+
+    @Test
+    @Ignore
+    public void testTrailingDotDotWithSuffix() {
+        RequestPathInfo p = createResourceUri("/some/path", "../suffix");
+        assertEquals("/some/path", p.getResourcePath());
+        assertNull("Selectors are null", p.getSelectorString());
+        assertEquals(0, p.getSelectors().length);
+        assertNull("Extension is null", p.getExtension());
+        assertEquals("/suffix", p.getSuffix());
+    }
+
+    @Test
+    public void testTrailingDotDotDot() {
+        RequestPathInfo p = createResourceUri("/some/path", "...");
+        assertEquals("/some/path", p.getResourcePath());
+        assertNull("Selectors are null", p.getSelectorString());
+        assertEquals(0, p.getSelectors().length);
+        assertNull("Extension is null", p.getExtension());
+        assertNull("Suffix is null", p.getSuffix());
+    }
+
+    @Test
+    public void testTrailingDotDotDotWithSuffix() {
+        RequestPathInfo p = createResourceUri("/some/path", ".../suffix");
+        assertEquals("/some/path", p.getResourcePath());
+        assertNull("Selectors are null", p.getSelectorString());
+        assertEquals(0, p.getSelectors().length);
+        assertNull("Extension is null", p.getExtension());
+        assertEquals("/suffix", p.getSuffix());
+    }
+
+    @Test
+    public void testAllOptions() {
+        RequestPathInfo p = createResourceUri("/some/path", ".print.a4.html/some/suffix");
+        assertEquals("/some/path", p.getResourcePath());
+        assertEquals("print.a4", p.getSelectorString());
+        assertEquals(2, p.getSelectors().length);
+        assertEquals("print", p.getSelectors()[0]);
+        assertEquals("a4", p.getSelectors()[1]);
+        assertEquals("html", p.getExtension());
+        assertEquals("/some/suffix", p.getSuffix());
+    }
+
+    @Test
+    public void testAllEmpty() {
+        RequestPathInfo p = createResourceUri("/", null);
+        assertEquals("/", p.getResourcePath());
+        assertNull("Selectors are null", p.getSelectorString());
+        assertEquals(0, p.getSelectors().length);
+        assertNull("Extension is null", p.getExtension());
+        assertNull("Suffix is null", p.getSuffix());
+    }
+
+    @Test
+    public void testPathOnly() {
+        RequestPathInfo p = createResourceUri("/some/path/here", "");
+        assertEquals("/some/path/here", p.getResourcePath());
+        assertNull("Selectors are null", p.getSelectorString());
+        assertEquals(0, p.getSelectors().length);
+        assertNull("Extension is null", p.getExtension());
+        assertNull("Suffix is null", p.getSuffix());
+    }
+
+    @Test
+    public void testPathWithExtensionOnly() {
+        RequestPathInfo p = createResourceUri("/some/path/here.html", "");
+        assertEquals("/some/path/here.html", p.getResourcePath());
+        assertNull("Selectors are null", p.getSelectorString());
+        assertEquals(0, p.getSelectors().length);
+        assertNull("Extension is null", p.getExtension());
+        assertNull("Suffix is null", p.getSuffix());
+    }
+
+    @Test
+    public void testPathAndExtensionOnly() {
+        RequestPathInfo p = createResourceUri("/some/path/here", ".html");
+        assertEquals("/some/path/here", p.getResourcePath());
+        assertNull("Selectors are null", p.getSelectorString());
+        assertEquals(0, p.getSelectors().length);
+        assertEquals("html", p.getExtension());
+        assertNull("Suffix is null", p.getSuffix());
+    }
+
+    @Test
+    public void testPathAndOneSelectorOnly() {
+        RequestPathInfo p = createResourceUri("/some/path/here", ".print.html");
+        assertEquals("/some/path/here", p.getResourcePath());
+        assertEquals("print", p.getSelectorString());
+        assertEquals(1, p.getSelectors().length);
+        assertEquals("print", p.getSelectors()[0]);
+        assertEquals("html", p.getExtension());
+        assertNull("Suffix is null", p.getSuffix());
+    }
+
+    @Test
+    public void testPathExtAndSuffix() {
+        RequestPathInfo p = createResourceUri("/some/path/here", ".html/something");
+        assertEquals("/some/path/here", p.getResourcePath());
+        assertNull("Selectors are null", p.getSelectorString());
+        assertEquals(0, p.getSelectors().length);
+        assertEquals("html", p.getExtension());
+        assertEquals("/something", p.getSuffix());
+    }
+
+    @Test
+    public void testSelectorsSplit() {
+        RequestPathInfo p = createResourceUri("/some/path", ".print.a4.html/some/suffix");
+        assertEquals("/some/path", p.getResourcePath());
+        assertEquals(2, p.getSelectors().length);
+        assertEquals("print", p.getSelectors()[0]);
+        assertEquals("a4", p.getSelectors()[1]);
+        assertEquals("html", p.getExtension());
+        assertEquals("/some/suffix", p.getSuffix());
+    }
+
+    @Test
+    public void testPartialResolutionB() {
+        RequestPathInfo p = createResourceUri("/some/path", ".print.a4.html/some/suffix");
+        assertEquals("/some/path", p.getResourcePath());
+        assertEquals("print.a4", p.getSelectorString());
+        assertEquals(2, p.getSelectors().length);
+        assertEquals("print", p.getSelectors()[0]);
+        assertEquals("a4", p.getSelectors()[1]);
+        assertEquals("html", p.getExtension());
+        assertEquals("/some/suffix", p.getSuffix());
+    }
+
+    @Test
+    public void testPartialResolutionC() {
+        RequestPathInfo p = createResourceUri("/some/path.print", ".a4.html/some/suffix");
+        assertEquals("/some/path.print", p.getResourcePath());
+        assertEquals("a4", p.getSelectorString());
+        assertEquals(1, p.getSelectors().length);
+        assertEquals("a4", p.getSelectors()[0]);
+        assertEquals("html", p.getExtension());
+        assertEquals("/some/suffix", p.getSuffix());
+    }
+
+    @Test
+    public void testPartialResolutionD() {
+        RequestPathInfo p = createResourceUri("/some/path.print.a4", ".html/some/suffix");
+        assertEquals("/some/path.print.a4", p.getResourcePath());
+        assertNull("Selectors are null", p.getSelectorString());
+        assertEquals(0, p.getSelectors().length);
+        assertEquals("html", p.getExtension());
+        assertEquals("/some/suffix", p.getSuffix());
+    }
+
+    @Test
+    public void testDotsAroundSuffix() {
+        RequestPathInfo p = createResourceUri("/libs/foo/content/something/formitems", ".json/image/vnd/xnd/knd.xml");
+        assertEquals("/libs/foo/content/something/formitems", p.getResourcePath());
+        assertEquals("json", p.getExtension());
+        assertNull("Selectors are null", p.getSelectorString());
+        assertEquals("/image/vnd/xnd/knd.xml", p.getSuffix());
+    }
+
+    @Test
+    public void testJIRA_250_a() {
+        RequestPathInfo p = createResourceUri("/bunkai", ".1.json");
+        assertEquals("/bunkai", p.getResourcePath());
+        assertEquals("json", p.getExtension());
+        assertEquals("1", p.getSelectorString());
+    }
+
+    @Test
+    public void testJIRA_250_b() {
+        RequestPathInfo p = createResourceUri("/", ".1.json");
+        assertEquals("/", p.getResourcePath());
+        assertEquals("json", p.getExtension());
+        assertNull("Suffix is null", p.getSuffix());
+        assertEquals("Selector string must not be null", "1",
+                p.getSelectorString());
+    }
+
+    @Test
+    public void testJIRA_250_c() {
+        RequestPathInfo p = createResourceUri("/", ".1.json/my/suffix");
+        assertEquals("/", p.getResourcePath());
+        assertEquals("json", p.getExtension());
+        assertEquals("/my/suffix", p.getSuffix());
+        assertEquals("Selector string must not be null", "1",
+                p.getSelectorString());
+    }
+
+    @Test
+    public void testJIRA_250_d() {
+        RequestPathInfo p = createResourceUri("/", ".json");
+        assertEquals("/", p.getResourcePath());
+        assertEquals("json", p.getExtension());
+        assertNull("Suffix is null", p.getSuffix());
+        assertNull("Selectors are null", p.getSelectorString());
+    }
+
+}