Author: radu
Date: Mon Jul 31 16:23:38 2017
New Revision: 1803550

URL: http://svn.apache.org/viewvc?rev=1803550&view=rev
Log:
SLING-7000 - HTL: URIManipulatorFilterExtension destroys opaque URIs

* applied slightly modified patch submitted by Dirk Rudolph

Added:
    
sling/trunk/bundles/scripting/sightly/engine/src/test/java/org/apache/sling/scripting/sightly/impl/engine/extension/ModifiableRequestPathInfoTest.java
Removed:
    
sling/trunk/bundles/scripting/sightly/engine/src/test/java/org/apache/sling/scripting/sightly/impl/utils/PathInfoTest.java
Modified:
    
sling/trunk/bundles/scripting/sightly/engine/src/main/java/org/apache/sling/scripting/sightly/impl/engine/extension/URIManipulationFilterExtension.java
    
sling/trunk/bundles/scripting/sightly/engine/src/test/java/org/apache/sling/scripting/sightly/impl/engine/extension/URIManipulationFilterExtensionTest.java
    sling/trunk/bundles/scripting/sightly/testing-content/pom.xml
    sling/trunk/bundles/scripting/sightly/testing/pom.xml

Modified: 
sling/trunk/bundles/scripting/sightly/engine/src/main/java/org/apache/sling/scripting/sightly/impl/engine/extension/URIManipulationFilterExtension.java
URL: 
http://svn.apache.org/viewvc/sling/trunk/bundles/scripting/sightly/engine/src/main/java/org/apache/sling/scripting/sightly/impl/engine/extension/URIManipulationFilterExtension.java?rev=1803550&r1=1803549&r2=1803550&view=diff
==============================================================================
--- 
sling/trunk/bundles/scripting/sightly/engine/src/main/java/org/apache/sling/scripting/sightly/impl/engine/extension/URIManipulationFilterExtension.java
 (original)
+++ 
sling/trunk/bundles/scripting/sightly/engine/src/main/java/org/apache/sling/scripting/sightly/impl/engine/extension/URIManipulationFilterExtension.java
 Mon Jul 31 16:23:38 2017
@@ -21,23 +21,30 @@ package org.apache.sling.scripting.sight
 import java.io.UnsupportedEncodingException;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.net.URLDecoder;
 import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
 import java.util.Map;
-import java.util.Set;
 
-import org.apache.commons.io.FilenameUtils;
+import javax.annotation.Nonnull;
+
 import org.apache.commons.lang.StringUtils;
+import org.apache.sling.api.request.RequestPathInfo;
+import org.apache.sling.api.resource.Resource;
 import org.apache.sling.scripting.sightly.SightlyException;
 import org.apache.sling.scripting.sightly.compiler.RuntimeFunction;
 import org.apache.sling.scripting.sightly.extension.RuntimeExtension;
 import org.apache.sling.scripting.sightly.render.RenderContext;
 import org.apache.sling.scripting.sightly.render.RuntimeObjectModel;
 import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 @Component(
         service = RuntimeExtension.class,
@@ -64,6 +71,7 @@ public class URIManipulationFilterExtens
     public static final String ADD_QUERY = "addQuery";
     public static final String REMOVE_QUERY = "removeQuery";
 
+    private static final Logger LOG = 
LoggerFactory.getLogger(URIManipulationFilterExtension.class);
 
     @Override
     @SuppressWarnings("unchecked")
@@ -72,156 +80,224 @@ public class URIManipulationFilterExtens
         RuntimeObjectModel runtimeObjectModel = renderContext.getObjectModel();
         String uriString = runtimeObjectModel.toString(arguments[0]);
         Map<String, Object> options = runtimeObjectModel.toMap(arguments[1]);
-        StringBuilder sb = new StringBuilder();
-        PathInfo pathInfo = new PathInfo(uriString);
-        uriAppender(sb, SCHEME, options, pathInfo.getScheme());
-        if (sb.length() > 0) {
-            sb.append(":");
-            
sb.append(StringUtils.defaultIfEmpty(pathInfo.getBeginPathSeparator(), "//"));
-        }
-        if (sb.length() > 0) {
-            uriAppender(sb, DOMAIN, options, pathInfo.getHost());
-        } else {
-            String domain = getOption(DOMAIN, options, pathInfo.getHost());
-            if (StringUtils.isNotEmpty(domain)) {
-                sb.append("//").append(domain);
-            }
-        }
-        if (pathInfo.getPort() > -1) {
-            sb.append(":").append(pathInfo.getPort());
-        }
-        String prependPath = getOption(PREPEND_PATH, options, 
StringUtils.EMPTY);
-        if (prependPath == null) {
-            prependPath = StringUtils.EMPTY;
-        }
-        String path = getOption(PATH, options, pathInfo.getPath());
-        if (StringUtils.isEmpty(path)) {
-            // if the path is forced to be empty don't remove the path
-            path = pathInfo.getPath();
-        }
-        if (StringUtils.isNotEmpty(path) && !"/".equals(path)) {
-            if (StringUtils.isNotEmpty(prependPath)) {
-                if (sb.length() > 0 && !prependPath.startsWith("/")) {
-                    prependPath = "/" + prependPath;
-                }
-                if (!prependPath.endsWith("/")) {
-                    prependPath += "/";
-                }
+
+        try {
+            URI originalUri = new URI(uriString);
+            // read in 
https://docs.oracle.com/javase/7/docs/api/java/net/URI.html in section 
"Identities"
+            // which constructors to use for which use case
+            final URI transformedUri;
+            final String scheme = getOption(SCHEME, options, 
originalUri.getScheme(), true);
+            final String fragment = getOption(FRAGMENT, options, 
originalUri.getFragment(), false);
+
+            // first check which type of URI
+            if (originalUri.isOpaque()) {
+                // only scheme and fragment is relevant
+                transformedUri = new URI(scheme, 
originalUri.getSchemeSpecificPart(), fragment);
+                return transformedUri.toString();
+            } else {
+                // only server-based authorities are supported
+                try {
+                    originalUri = originalUri.parseServerAuthority();
+                } catch (URISyntaxException e) {
+                    LOG.warn("Only server-based authorities are supported for 
non-opaque URLs");
+                    throw e;
+                }
+                final String host = getOption(DOMAIN, options, 
originalUri.getHost(), true);
+                final String path = getPath(runtimeObjectModel, 
originalUri.getPath(), options, scheme != null || host != null);
+                final String escapedQuery = 
getEscapedQuery(runtimeObjectModel, originalUri.getRawQuery(), options);
+
+                // the URI constructor will escape the % in the query part 
again, we must revert that
+                transformedUri = new URI(scheme, originalUri.getUserInfo(), 
host, originalUri.getPort(), path, escapedQuery,
+                        fragment);
+                return unescapePercentInQuery(transformedUri.toString());
             }
+        } catch (URISyntaxException e) {
+            LOG.warn("Cannot manipulate invalid URI '{}'", uriString, e);
+        }
+        return uriString;
+    }
 
+    /**
+     * Decodes the encoding of the percent character itself within the given 
uri's query i.e. reverts the conversion of {@code %} to
+     * {@code %25}.
+     *
+     * @return the uri with the query partly decoded ({@code %25} decoded to 
{@code %})
+     * @see <a 
href="https://blog.stackhunter.com/2014/03/31/encode-special-characters-java-net-uri/";>How
 to Encode Special Characters in
+     * Java’s URI Class</a>
+     * @see <a 
href="https://stackoverflow.com/q/19917079/5155923";>java.net.URI and percent in 
query parameter value</a>
+     */
+    static String unescapePercentInQuery(String uri) {
+        String[] parts = uri.split("\\?", 2);
+        if (parts.length != 2) {
+            return uri;
+        }
+        // separate fragment
+        String[] suffixParts = parts[1].split("#", 2);
+        final String suffix;
+        final String query;
+        if (suffixParts.length == 2) {
+            query = suffixParts[0];
+            suffix = "#" + suffixParts[1];
+        } else {
+            query = parts[1];
+            suffix = "";
+        }
+        return parts[0] + "?" + query.replaceAll("%25", "%") + suffix;
+    }
 
-            String appendPath = getOption(APPEND_PATH, options, 
StringUtils.EMPTY);
-            if (appendPath == null) {
-                appendPath = StringUtils.EMPTY;
-            }
-            if (StringUtils.isNotEmpty(appendPath)) {
-                if (!appendPath.startsWith("/")) {
-                    appendPath = "/" + appendPath;
-                }
-            }
-            String newPath;
-            try {
-                newPath = new URI(prependPath + path + 
appendPath).normalize().getPath();
-            } catch (URISyntaxException e) {
-                newPath = prependPath + path + appendPath;
-            }
-            if (sb.length() > 0 && sb.lastIndexOf("/") != sb.length() - 1 && 
StringUtils.isNotEmpty(newPath) && !newPath.startsWith("/")) {
-                sb.append("/");
-            }
-            sb.append(newPath);
-            Set<String> selectors = pathInfo.getSelectors();
-            handleSelectors(runtimeObjectModel, selectors, options);
-            for (String selector : selectors) {
-                if (StringUtils.isNotBlank(selector) && !selector.contains(" 
")) {
-                    // make sure not to append empty or invalid selectors
-                    sb.append(".").append(selector);
-                }
-            }
-            String extension = getOption(EXTENSION, options, 
pathInfo.getExtension());
-            if (StringUtils.isNotEmpty(extension)) {
-                sb.append(".").append(extension);
+    /**
+     * Returns a value from a map.
+     *
+     * @param key               the option name
+     * @param options           the options map
+     * @param defaultValue      the default value to return
+     * @param useDefaultIfEmpty if set to {@code true}, the {@code 
defaultValue} will be returned when the map contains the required
+     *                          option but its value is the empty string or 
null
+     * @return either the value from the map for entry with name or {@code 
defaultValue}; in case {@code useDefaultIfEmpty} is set to
+     * {@code false}, returns null in case the entry present in the map but 
has an empty string or null value
+     */
+    private String getOption(String key, Map<String, Object> options, String 
defaultValue, boolean useDefaultIfEmpty) {
+        String value = (String) options.get(key);
+        if (StringUtils.isNotEmpty(value)) {
+            return value;
+        }
+        if (options.containsKey(key)) {
+            if (useDefaultIfEmpty) {
+                return defaultValue;
+            } else {
+                return null;
             }
+        }
+        return defaultValue;
+    }
 
-            String prependSuffix = getOption(PREPEND_SUFFIX, options, 
StringUtils.EMPTY);
-            if (StringUtils.isNotEmpty(prependSuffix)) {
-                if (!prependSuffix.startsWith("/")) {
-                    prependSuffix = "/" + prependSuffix;
-                }
-                if (!prependSuffix.endsWith("/")) {
-                    prependSuffix += "/";
-                }
-            }
-            String pathInfoSuffix = pathInfo.getSuffix();
-            String suffix = getOption(SUFFIX, options, pathInfoSuffix == null 
? StringUtils.EMPTY : pathInfoSuffix);
-            if (suffix == null) {
-                suffix = StringUtils.EMPTY;
-            }
-            String appendSuffix = getOption(APPEND_SUFFIX, options, 
StringUtils.EMPTY);
-            if (StringUtils.isNotEmpty(appendSuffix)) {
-                appendSuffix = "/" + appendSuffix;
-            }
-            String newSuffix = FilenameUtils.normalize(prependSuffix + suffix 
+ appendSuffix, true);
-            if (StringUtils.isNotEmpty(newSuffix)) {
-                if (!newSuffix.startsWith("/")) {
+    static String concatenateWithSlashes(String... pathParts) {
+        StringBuilder sb = new StringBuilder();
+        for (String pathPart : pathParts) {
+            if (StringUtils.isNotBlank(pathPart)) {
+                if (sb.length() > 0 && !pathPart.startsWith("/") && 
!sb.toString().endsWith("/")) {
                     sb.append("/");
                 }
-                sb.append(newSuffix);
-            }
-
-        } else if ("/".equals(path)) {
-            sb.append(path);
-        }
-        Map<String, Collection<String>> parameters = pathInfo.getParameters();
-        handleParameters(runtimeObjectModel, parameters, options);
-        if (sb.length() > 0 && !parameters.isEmpty()) {
-            if (StringUtils.isEmpty(path)) {
-                sb.append("/");
-            }
-            sb.append("?");
-            for (Map.Entry<String, Collection<String>> entry : 
parameters.entrySet()) {
-                for (String value : entry.getValue()) {
-                    
sb.append(entry.getKey()).append("=").append(value).append("&");
+                if (sb.toString().endsWith("/") && pathPart.startsWith("/")) {
+                    sb.append(pathPart.substring(1));
+                } else {
+                    sb.append(pathPart);
                 }
             }
-            // delete the last &
-            sb.deleteCharAt(sb.length() - 1);
-        }
-        String fragment = getOption(FRAGMENT, options, pathInfo.getFragment());
-        if (StringUtils.isNotEmpty(fragment)) {
-            sb.append("#").append(fragment);
         }
         return sb.toString();
     }
 
-    private void uriAppender(StringBuilder stringBuilder, String option, 
Map<String, Object> options, String defaultValue) {
-        String value = (String) options.get(option);
-        if (StringUtils.isNotEmpty(value)) {
-            stringBuilder.append(value);
+    private String getPath(RuntimeObjectModel runtimeObjectModel, String 
originalPath, Map<String, Object> options, boolean isAbsolute) {
+        ModifiableRequestPathInfo requestPathInfo = new 
ModifiableRequestPathInfo(originalPath);
+        final String prependPath = getOption(PREPEND_PATH, options, 
StringUtils.EMPTY, true);
+        final String path =
+                getOption(PATH, options, requestPathInfo.getResourcePath(), 
true); // empty path option should not remove existing path!
+        final String appendPath = getOption(APPEND_PATH, options, 
StringUtils.EMPTY, true);
+        if (!options.containsKey(PATH) && StringUtils.isEmpty(path)) {
+            // no not prepend/append if path is neither set initially nor 
through option
+            LOG.debug("Do not modify path because original path was empty and 
not set through an option either!");
+            // dealing with selectors, extension or suffix is not allowed then 
either
+            return requestPathInfo.toString();
         } else {
-            if (StringUtils.isNotEmpty(defaultValue)) {
-                stringBuilder.append(defaultValue);
+            String newPath = concatenateWithSlashes(prependPath, path, 
appendPath);
+
+            // do we need to make the path absolute?
+            if (isAbsolute && !newPath.startsWith("/")) {
+                newPath = '/' + newPath;
+            }
+            // modify resource path
+            requestPathInfo.setResourcePath(newPath);
+        }
+
+        handleSelectors(runtimeObjectModel, requestPathInfo, options);
+        // handle extension
+        String extension = getOption(EXTENSION, options, 
requestPathInfo.getExtension(), false);
+        requestPathInfo.setExtension(extension);
+
+        // modify suffix!
+        String prependSuffix = getOption(PREPEND_SUFFIX, options, 
StringUtils.EMPTY, true);
+        // remove suffix if option is empty
+        String suffix = getOption(SUFFIX, options, 
requestPathInfo.getSuffix(), false);
+        String appendSuffix = getOption(APPEND_SUFFIX, options, 
StringUtils.EMPTY, true);
+
+        String newSuffix = concatenateWithSlashes(prependSuffix, suffix, 
appendSuffix);
+        if (StringUtils.isNotEmpty(newSuffix)) {
+            // make sure it starts with a slash
+            if (!newSuffix.startsWith("/")) {
+                newSuffix = '/' + newSuffix;
             }
         }
+        requestPathInfo.setSuffix(newSuffix);
+        return requestPathInfo.toString();
     }
 
-    private String getOption(String option, Map<String, Object> options, 
String defaultValue) {
-        if (options.containsKey(option)) {
-            return (String) options.get(option);
+    private String getEscapedQuery(RuntimeObjectModel runtimeObjectModel, 
String originalQuery, Map<String, Object> options) {
+        // parse parameters
+        Map<String, Collection<String>> parameters = new LinkedHashMap<>();
+        if (StringUtils.isNotEmpty(originalQuery)) {
+            String[] keyValuePairs = originalQuery.split("&");
+            for (String keyValuePair : keyValuePairs) {
+                String[] pair = keyValuePair.split("=");
+                if (pair.length == 2) {
+                    String param;
+                    try {
+                        param = URLDecoder.decode(pair[0], 
StandardCharsets.UTF_8.name());
+                    } catch (UnsupportedEncodingException e) {
+                        LOG.warn("Could not decode parameter key '{}'", 
pair[0], e);
+                        continue;
+                    }
+                    String value;
+                    try {
+                        value = URLDecoder.decode(pair[1], 
StandardCharsets.UTF_8.name());
+                    } catch (UnsupportedEncodingException e) {
+                        LOG.warn("Could not decode parameter value of 
parameter '{}': '{}'", param, pair[1], e);
+                        continue;
+                    }
+                    Collection<String> values = parameters.get(param);
+                    if (values == null) {
+                        values = new ArrayList<>();
+                        parameters.put(param, values);
+                    }
+                    values.add(value);
+                }
+            }
+        }
 
+        if (handleParameters(runtimeObjectModel, parameters, options)) {
+            if (!parameters.isEmpty()) {
+                try {
+                    StringBuilder sb = new StringBuilder();
+                    for (Map.Entry<String, Collection<String>> entry : 
parameters.entrySet()) {
+                        for (String value : entry.getValue()) {
+                            sb.append(URLEncoder.encode(entry.getKey(), 
StandardCharsets.UTF_8.name())).append("=")
+                                    .append(URLEncoder.encode(value, 
StandardCharsets.UTF_8.name())).append("&");
+                        }
+                    }
+                    // delete the last &
+                    sb.deleteCharAt(sb.length() - 1);
+                    return sb.toString();
+                } catch (UnsupportedEncodingException e) {
+                    throw new SightlyException("Could not encode the parameter 
values/keys", e);
+                }
+            } else {
+                return null;
+            }
         }
-        return defaultValue;
+        return originalQuery;
     }
 
-    private void handleSelectors(RuntimeObjectModel runtimeObjectModel, 
Set<String> selectors, Map<String, Object> options) {
+    private void handleSelectors(RuntimeObjectModel runtimeObjectModel, 
ModifiableRequestPathInfo requestPathInfo,
+                                 Map<String, Object> options) {
         if (options.containsKey(SELECTORS)) {
             Object selectorsOption = options.get(SELECTORS);
             if (selectorsOption == null) {
                 // we want to remove all selectors
-                selectors.clear();
+                requestPathInfo.clearSelectors();
             } else if (selectorsOption instanceof String) {
                 String selectorString = (String) selectorsOption;
                 String[] selectorsArray = selectorString.split("\\.");
-                replaceSelectors(selectors, selectorsArray);
+                requestPathInfo.replaceSelectors(selectorsArray);
             } else if (selectorsOption instanceof Object[]) {
                 Object[] selectorsURIArray = (Object[]) selectorsOption;
                 String[] selectorsArray = new String[selectorsURIArray.length];
@@ -229,14 +305,14 @@ public class URIManipulationFilterExtens
                 for (Object selector : selectorsURIArray) {
                     selectorsArray[index++] = 
runtimeObjectModel.toString(selector);
                 }
-                replaceSelectors(selectors, selectorsArray);
+                requestPathInfo.replaceSelectors(selectorsArray);
             }
         }
         Object addSelectorsOption = options.get(ADD_SELECTORS);
         if (addSelectorsOption instanceof String) {
             String selectorString = (String) addSelectorsOption;
             String[] selectorsArray = selectorString.split("\\.");
-            addSelectors(selectors, selectorsArray);
+            requestPathInfo.addSelectors(selectorsArray);
         } else if (addSelectorsOption instanceof Object[]) {
             Object[] selectorsURIArray = (Object[]) addSelectorsOption;
             String[] selectorsArray = new String[selectorsURIArray.length];
@@ -244,13 +320,13 @@ public class URIManipulationFilterExtens
             for (Object selector : selectorsURIArray) {
                 selectorsArray[index++] = 
runtimeObjectModel.toString(selector);
             }
-            addSelectors(selectors, selectorsArray);
+            requestPathInfo.addSelectors(selectorsArray);
         }
         Object removeSelectorsOption = options.get(REMOVE_SELECTORS);
         if (removeSelectorsOption instanceof String) {
             String selectorString = (String) removeSelectorsOption;
             String[] selectorsArray = selectorString.split("\\.");
-            removeSelectors(selectors, selectorsArray);
+            requestPathInfo.removeSelectors(selectorsArray);
         } else if (removeSelectorsOption instanceof Object[]) {
             Object[] selectorsURIArray = (Object[]) removeSelectorsOption;
             String[] selectorsArray = new String[selectorsURIArray.length];
@@ -258,37 +334,26 @@ public class URIManipulationFilterExtens
             for (Object selector : selectorsURIArray) {
                 selectorsArray[index++] = 
runtimeObjectModel.toString(selector);
             }
-            removeSelectors(selectors, selectorsArray);
+            requestPathInfo.removeSelectors(selectorsArray);
         }
-
-    }
-
-    private void replaceSelectors(Set<String> selectors, String[] 
selectorsArray) {
-        selectors.clear();
-        selectors.addAll(Arrays.asList(selectorsArray));
-    }
-
-    private void addSelectors(Set<String> selectors, String[] selectorsArray) {
-        selectors.addAll(Arrays.asList(selectorsArray));
-    }
-
-    private void removeSelectors(Set<String> selectors, String[] 
selectorsArray) {
-        selectors.removeAll(Arrays.asList(selectorsArray));
     }
 
     @SuppressWarnings("unchecked")
-    private void handleParameters(RuntimeObjectModel runtimeObjectModel, 
Map<String, Collection<String>> parameters, Map<String, Object>
-            options) {
+    private boolean handleParameters(RuntimeObjectModel runtimeObjectModel, 
Map<String, Collection<String>> parameters,
+                                     Map<String, Object> options) {
+        boolean hasModifiedParameters = false;
         if (options.containsKey(QUERY)) {
             Object queryOption = options.get(QUERY);
             parameters.clear();
             Map<String, Object> queryParameters = 
runtimeObjectModel.toMap(queryOption);
             addQueryParameters(runtimeObjectModel, parameters, 
queryParameters);
+            hasModifiedParameters = true;
         }
         Object addQueryOption = options.get(ADD_QUERY);
         if (addQueryOption != null) {
             Map<String, Object> addParams = 
runtimeObjectModel.toMap(addQueryOption);
             addQueryParameters(runtimeObjectModel, parameters, addParams);
+            hasModifiedParameters = true;
         }
         Object removeQueryOption = options.get(REMOVE_QUERY);
         if (removeQueryOption != null) {
@@ -303,177 +368,106 @@ public class URIManipulationFilterExtens
                     }
                 }
             }
+            hasModifiedParameters = true;
         }
+        return hasModifiedParameters;
     }
 
-    private void addQueryParameters(RuntimeObjectModel runtimeObjectModel, 
Map<String, Collection<String>> parameters, Map<String, Object>
-            queryParameters) {
+    private void addQueryParameters(RuntimeObjectModel runtimeObjectModel, 
Map<String, Collection<String>> parameters,
+                                    Map<String, Object> queryParameters) {
         for (Map.Entry<String, Object> entry : queryParameters.entrySet()) {
             Object entryValue = entry.getValue();
-            try {
-                if (runtimeObjectModel.isCollection(entryValue)) {
-                    Collection<Object> collection = 
runtimeObjectModel.toCollection(entryValue);
-                    Collection<String> values = new 
ArrayList<>(collection.size());
-                    for (Object o : collection) {
-                        
values.add(URLEncoder.encode(runtimeObjectModel.toString(o), "UTF-8"));
-                    }
-                    parameters.put(entry.getKey(), values);
-                } else {
-                    Collection<String> values = new ArrayList<>(1);
-                    
values.add(URLEncoder.encode(runtimeObjectModel.toString(entryValue), "UTF-8"));
-                    parameters.put(entry.getKey(), values);
+            if (runtimeObjectModel.isCollection(entryValue)) {
+                Collection<Object> collection = 
runtimeObjectModel.toCollection(entryValue);
+                Collection<String> values = new ArrayList<>(collection.size());
+                for (Object o : collection) {
+                    values.add(runtimeObjectModel.toString(o));
                 }
-            } catch (UnsupportedEncodingException e) {
-                throw new SightlyException(e);
+                parameters.put(entry.getKey(), values);
+            } else {
+                Collection<String> values = new ArrayList<>(1);
+                values.add(runtimeObjectModel.toString(entryValue));
+                parameters.put(entry.getKey(), values);
             }
         }
     }
 
-    public static class PathInfo {
+    static class ModifiableRequestPathInfo implements RequestPathInfo {
 
-        private URI uri;
-        private String path;
-        private Set<String> selectors;
-        private String selectorString;
+        private String resourcePath;
+        private List<String> selectors;
         private String extension;
         private String suffix;
-        private Map<String, Collection<String>> parameters = new 
LinkedHashMap<>();
 
         /**
-         * Creates a {@code PathInfo} object based on a request path.
+         * Creates the {@link RequestPathInfo} object based on a request path 
only.
+         * This utility does not take this into account any underlying 
repository structure and
+         * just uses the first dot to split resource path from the rest!
+         * {@code org.apache.sling.servlets.resolver.internal.DecomposedUrl} 
uses the same logic!
          *
          * @param path the full normalized path (no '.', '..', or double 
slashes8) of the request, including the query parameters
          * @throws NullPointerException if the supplied {@code path} is null
          */
-        public PathInfo(String path) {
+        ModifiableRequestPathInfo(String path) {
             if (path == null) {
                 throw new NullPointerException("The path parameter cannot be 
null.");
             }
-            try {
-                uri = new URI(path);
-            } catch (URISyntaxException e) {
-                throw new SightlyException("The provided path does not 
represent a valid URI: " + path);
-            }
-            selectors = new LinkedHashSet<>();
-            String processingPath = path;
-            if (uri.getPath() != null) {
-                processingPath = uri.getPath();
-            }
-            int lastDot = processingPath.lastIndexOf('.');
-            if (lastDot > -1) {
-                String afterLastDot = processingPath.substring(lastDot + 1);
-                String[] parts = afterLastDot.split("/");
-                extension = parts[0];
-                if (parts.length > 1) {
-                    // we have a suffix
-                    StringBuilder suffixSB = new StringBuilder();
-                    for (int i = 1; i < parts.length; i++) {
-                        suffixSB.append("/").append(parts[i]);
-                    }
-                    int hashIndex = suffixSB.indexOf("#");
-                    if (hashIndex > -1) {
-                        suffix = suffixSB.substring(0, hashIndex);
-                    } else {
-                        suffix = suffixSB.toString();
-                    }
-                }
-            }
-            int firstDot = processingPath.indexOf('.');
-            if (firstDot < lastDot) {
-                selectorString = processingPath.substring(firstDot + 1, 
lastDot);
-                String[] selectorsArray = selectorString.split("\\.");
-                selectors.addAll(Arrays.asList(selectorsArray));
-            }
-            int pathLength = processingPath.length() - (selectorString == null 
? 0 : selectorString.length() + 1) - (extension == null ? 0:
-                    extension.length() + 1) - (suffix == null ? 0 : 
suffix.length());
-            if (pathLength == processingPath.length()) {
-                this.path = processingPath;
+            selectors = new LinkedList<>();
+            // get relevant parts
+            int firstDot = path.indexOf('.');
+
+            // the extra path in the request URI
+            final String pathToParse;
+            if (firstDot >= 0) {
+                pathToParse = path.substring(firstDot);
+                resourcePath = path.substring(0, firstDot);
             } else {
-                this.path = processingPath.substring(0, pathLength);
+                pathToParse = "";
+                resourcePath = path;
             }
-            String query = uri.getRawQuery();
-            if (StringUtils.isNotEmpty(query)) {
-                String[] keyValuePairs = query.split("&");
-                for (String keyValuePair : keyValuePairs) {
-                    String[] pair = keyValuePair.split("=");
-                    if (pair.length == 2) {
-                        String param = pair[0];
-                        String value = pair[1];
-                        Collection<String> values = parameters.get(param);
-                        if (values == null) {
-                            values = new ArrayList<>();
-                            parameters.put(param, values);
-                        }
-                        values.add(value);
-                    }
-                }
+
+            // use logic from 
org.apache.sling.engine.impl.request.SlingRequestPathInfo
+            // separate selectors/ext from the suffix
+            int firstSlash = pathToParse.indexOf('/');
+            String pathToSplit;
+            if (firstSlash < 0) {
+                pathToSplit = pathToParse;
+                suffix = null;
+            } else {
+                pathToSplit = pathToParse.substring(0, firstSlash);
+                suffix = pathToParse.substring(firstSlash);
             }
-        }
 
-        /**
-         * Returns the scheme of this path if the path corresponds to a URI 
and if the URI provides scheme information.
-         *
-         * @return the scheme or {@code null} if the path does not contain a 
scheme
-         */
-        public String getScheme() {
-            return uri.getScheme();
-        }
+            int lastDot = pathToSplit.lastIndexOf('.');
+
+            if (lastDot > 1) {
+                // no selectors if splitting would give an empty array
+                String tmpSel = pathToSplit.substring(1, lastDot);
+                selectors.addAll(Arrays.asList(tmpSel.split("\\.")));
 
-        /**
-         * Returns the path separator ("//") if the path defines an absolute 
URI.
-         *
-         * @return the path separator if the path is an absolute URI, {@code 
null} otherwise
-         */
-        public String getBeginPathSeparator() {
-            if (uri.isAbsolute()) {
-                return "//";
             }
-            return null;
-        }
 
-        /**
-         * Returns the host part of the path, if the path defines a URI.
-         *
-         * @return the host if the path defines a URI, {@code null} otherwise
-         */
-        public String getHost() {
-            return uri.getHost();
+            // extension only if lastDot is not trailing
+            extension = (lastDot + 1 < pathToSplit.length())
+                    ? pathToSplit.substring(lastDot + 1) : null;
         }
 
-        /**
-         * Returns the port if the path defines a URI and if it contains port 
information.
-         *
-         * @return the port or -1 if no port is defined
-         */
-        public int getPort() {
-            return uri.getPort();
+        void setExtension(String extension) {
+            this.extension = extension;
         }
 
-        /**
-         * Returns the path from which <i>{@code this}</i> object was built.
-         *
-         * @return the original path
-         */
-        public String getFullPath() {
-            return uri.toString();
+        void setSuffix(String suffix) {
+            this.suffix = suffix;
         }
 
-        /**
-         * Returns the path identifying the resource, without any selectors, 
extension or query parameters.
-         *
-         * @return the path of the resource
-         */
-        public String getPath() {
-            return path;
+        @Override
+        @Nonnull
+        public String getResourcePath() {
+            return resourcePath;
         }
 
-        /**
-         * Returns the selectors set.
-         *
-         * @return the selectors set; if there are no selectors the set will 
be empty
-         */
-        public Set<String> getSelectors() {
-            return selectors;
+        void setResourcePath(String path) {
+            this.resourcePath = path;
         }
 
         /**
@@ -481,6 +475,7 @@ public class URIManipulationFilterExtens
          *
          * @return the extension, if one exists, otherwise {@code null}
          */
+        @Override
         public String getExtension() {
             return extension;
         }
@@ -490,34 +485,68 @@ public class URIManipulationFilterExtens
          *
          * @return the selector string, if the path has selectors, otherwise 
{@code null}
          */
+        @Override
         public String getSelectorString() {
-            return selectorString;
+            throw new UnsupportedOperationException();
         }
 
         /**
-         * Returns the suffix appended to the path. The suffix represents the 
path segment between the path's extension and the path's fragment.
+         * Returns the suffix appended to the path. The suffix represents the 
path segment between the path's extension and the path's
+         * fragment.
          *
          * @return the suffix if the path contains one, {@code null} otherwise
          */
+        @Override
         public String getSuffix() {
             return suffix;
         }
 
-        /**
-         * Returns the fragment is this path defines a URI and it contains a 
fragment.
-         *
-         * @return the fragment, or {@code null} if one doesn't exist
-         */
-        public String getFragment() {
-            return uri.getFragment();
+        @Override
+        @Nonnull
+        public String[] getSelectors() {
+            return selectors.toArray(new String[]{});
+        }
+
+        @Override
+        public Resource getSuffixResource() {
+            throw new UnsupportedOperationException();
+        }
+
+        public String toString() {
+            // resourcePath + selectors + extension + suffix
+            StringBuilder sb = new StringBuilder(getResourcePath());
+            if (getSelectors().length > 0) {
+                for (String selector : selectors) {
+                    if (StringUtils.isNotBlank(selector) && 
!selector.contains(" ")) {
+                        // make sure not to append empty or invalid selectors
+                        sb.append(".").append(selector);
+                    }
+                }
+            }
+            if (StringUtils.isNotEmpty(getExtension())) {
+                sb.append('.').append(getExtension());
+            }
+            if (StringUtils.isNotEmpty(getSuffix())) {
+                sb.append(getSuffix());
+            }
+            return sb.toString();
         }
 
-        /**
-         * Returns the URI parameters if the provided path defines a URI.
-         * @return the parameters map; can be empty if there are no parameters 
of if the path doesn't identify a URI
-         */
-        public Map<String, Collection<String>> getParameters() {
-            return parameters;
+        void replaceSelectors(String[] selectorsArray) {
+            selectors.clear();
+            selectors.addAll(Arrays.asList(selectorsArray));
+        }
+
+        void addSelectors(String[] selectorsArray) {
+            selectors.addAll(Arrays.asList(selectorsArray));
+        }
+
+        void removeSelectors(String[] selectorsArray) {
+            selectors.removeAll(Arrays.asList(selectorsArray));
+        }
+
+        void clearSelectors() {
+            selectors.clear();
         }
     }
 

Added: 
sling/trunk/bundles/scripting/sightly/engine/src/test/java/org/apache/sling/scripting/sightly/impl/engine/extension/ModifiableRequestPathInfoTest.java
URL: 
http://svn.apache.org/viewvc/sling/trunk/bundles/scripting/sightly/engine/src/test/java/org/apache/sling/scripting/sightly/impl/engine/extension/ModifiableRequestPathInfoTest.java?rev=1803550&view=auto
==============================================================================
--- 
sling/trunk/bundles/scripting/sightly/engine/src/test/java/org/apache/sling/scripting/sightly/impl/engine/extension/ModifiableRequestPathInfoTest.java
 (added)
+++ 
sling/trunk/bundles/scripting/sightly/engine/src/test/java/org/apache/sling/scripting/sightly/impl/engine/extension/ModifiableRequestPathInfoTest.java
 Mon Jul 31 16:23:38 2017
@@ -0,0 +1,103 @@
+/*******************************************************************************
+ * 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.scripting.sightly.impl.engine.extension;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+public class ModifiableRequestPathInfoTest {
+
+    private static final String EMPTY = "";
+    private static final String PATH = "test";
+    private static final String SELECTOR_STRING = "a.b";
+    private static final String[] SELECTOR_STRING_SET = new String[]{"a", "b"};
+    private static final String EXTENSION = "html";
+    private static final String SUFFIX = "/suffix1/suffix2";
+
+    private static final 
URIManipulationFilterExtension.ModifiableRequestPathInfo emptyPathInfo =
+            new 
URIManipulationFilterExtension.ModifiableRequestPathInfo(EMPTY);
+    private static final 
URIManipulationFilterExtension.ModifiableRequestPathInfo simplePath =
+            new URIManipulationFilterExtension.ModifiableRequestPathInfo(PATH);
+    private static final 
URIManipulationFilterExtension.ModifiableRequestPathInfo
+            pathWithExtension = new 
URIManipulationFilterExtension.ModifiableRequestPathInfo(PATH + "." + 
EXTENSION);
+    private static final 
URIManipulationFilterExtension.ModifiableRequestPathInfo
+            pathWithSelectors =
+            new URIManipulationFilterExtension.ModifiableRequestPathInfo(PATH 
+ "." + SELECTOR_STRING + "." + EXTENSION);
+    private static final 
URIManipulationFilterExtension.ModifiableRequestPathInfo
+            pathWithSelectorsSuffix =
+            new URIManipulationFilterExtension.ModifiableRequestPathInfo(PATH 
+ "." + SELECTOR_STRING + "." + EXTENSION + SUFFIX);
+    private static final 
URIManipulationFilterExtension.ModifiableRequestPathInfo
+            pathWithMultipleDotsSuffix = new 
URIManipulationFilterExtension.ModifiableRequestPathInfo(
+            "test/child.name/resource." + SELECTOR_STRING + "." + EXTENSION + 
SUFFIX);
+
+
+    @Test
+    public void testGetResourcePath() throws Exception {
+        assertEquals(EMPTY, emptyPathInfo.getResourcePath());
+        assertEquals(PATH, simplePath.getResourcePath());
+        assertEquals(PATH, pathWithExtension.getResourcePath());
+        assertEquals(PATH, pathWithSelectors.getResourcePath());
+        assertEquals(PATH, pathWithSelectorsSuffix.getResourcePath());
+        assertEquals("test/child", 
pathWithMultipleDotsSuffix.getResourcePath());
+    }
+
+    @Test
+    public void testToString() throws Exception {
+        assertEquals(EMPTY, emptyPathInfo.toString());
+        assertEquals(PATH, simplePath.toString());
+        assertEquals(PATH + "." + EXTENSION, pathWithExtension.toString());
+        assertEquals(PATH + "." + SELECTOR_STRING + "." + EXTENSION, 
pathWithSelectors.toString());
+        assertEquals(PATH + "." + SELECTOR_STRING + "." + EXTENSION + SUFFIX, 
pathWithSelectorsSuffix.toString());
+        assertEquals("test/child.name/resource." + SELECTOR_STRING + "." + 
EXTENSION + SUFFIX, pathWithMultipleDotsSuffix.toString());
+    }
+
+    @Test
+    public void testGetSelectors() throws Exception {
+        assertEquals(0, emptyPathInfo.getSelectors().length);
+        assertEquals(0, simplePath.getSelectors().length);
+        assertEquals(0, pathWithExtension.getSelectors().length);
+        assertArrayEquals(SELECTOR_STRING_SET, 
pathWithSelectors.getSelectors());
+        assertArrayEquals(SELECTOR_STRING_SET, 
pathWithSelectorsSuffix.getSelectors());
+        assertEquals(0, pathWithMultipleDotsSuffix.getSelectors().length);
+    }
+
+    @Test
+    public void testGetExtension() throws Exception {
+        assertNull(emptyPathInfo.getExtension());
+        assertNull(simplePath.getExtension());
+        assertEquals(EXTENSION, pathWithExtension.getExtension());
+        assertEquals(EXTENSION, pathWithSelectors.getExtension());
+        assertEquals(EXTENSION, pathWithSelectorsSuffix.getExtension());
+        assertEquals("name", pathWithMultipleDotsSuffix.getExtension());
+    }
+
+    @Test
+    public void testGetSuffix() {
+        assertNull(emptyPathInfo.getSuffix());
+        assertNull(simplePath.getSuffix());
+        assertNull(pathWithExtension.getSuffix());
+        assertNull(pathWithSelectors.getSuffix());
+        assertEquals(SUFFIX, pathWithSelectorsSuffix.getSuffix());
+        assertEquals("/resource." + SELECTOR_STRING + "." + EXTENSION + 
SUFFIX, pathWithMultipleDotsSuffix.getSuffix());
+    }
+
+}

Modified: 
sling/trunk/bundles/scripting/sightly/engine/src/test/java/org/apache/sling/scripting/sightly/impl/engine/extension/URIManipulationFilterExtensionTest.java
URL: 
http://svn.apache.org/viewvc/sling/trunk/bundles/scripting/sightly/engine/src/test/java/org/apache/sling/scripting/sightly/impl/engine/extension/URIManipulationFilterExtensionTest.java?rev=1803550&r1=1803549&r2=1803550&view=diff
==============================================================================
--- 
sling/trunk/bundles/scripting/sightly/engine/src/test/java/org/apache/sling/scripting/sightly/impl/engine/extension/URIManipulationFilterExtensionTest.java
 (original)
+++ 
sling/trunk/bundles/scripting/sightly/engine/src/test/java/org/apache/sling/scripting/sightly/impl/engine/extension/URIManipulationFilterExtensionTest.java
 Mon Jul 31 16:23:38 2017
@@ -16,6 +16,8 @@
  
******************************************************************************/
 package org.apache.sling.scripting.sightly.impl.engine.extension;
 
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.util.LinkedHashMap;
 import javax.script.Bindings;
 import javax.script.SimpleBindings;
@@ -62,6 +64,25 @@ public class URIManipulationFilterExtens
     }
 
     @Test
+    public void testUnescapePercentInQuery() throws URISyntaxException {
+        URI testUri = new URI("http", null, "example.com", -1, 
"/example/search.html", "q=6%25-10%25", "fragment=%");
+        
assertEquals("http://example.com/example/search.html?q=6%25-10%25#fragment=%25";,
+                
URIManipulationFilterExtension.unescapePercentInQuery(testUri.toString()));
+        testUri = new URI("http", null, "example.com", -1, 
"/example/search.html", "q=6%25-10%25", null);
+        assertEquals("http://example.com/example/search.html?q=6%25-10%25";,
+                
URIManipulationFilterExtension.unescapePercentInQuery(testUri.toString()));
+    }
+
+    @Test
+    public void testConcatenateWithSlashes() {
+        assertEquals("a/b/c", 
URIManipulationFilterExtension.concatenateWithSlashes("a", "b", "c"));
+        assertEquals("a/b/c", 
URIManipulationFilterExtension.concatenateWithSlashes("a", "/b", "c"));
+        assertEquals("a/b/c", 
URIManipulationFilterExtension.concatenateWithSlashes("a", "/b", "/c"));
+        assertEquals("/b/", 
URIManipulationFilterExtension.concatenateWithSlashes("/", "b", "/"));
+        assertEquals("/one/two/", 
URIManipulationFilterExtension.concatenateWithSlashes("/one/", "/two/"));
+    }
+
+    @Test
     public void testPercentEncodedURLs_SLING_6761() {
         assertEquals(
                 "/example/search.html?q=6%25-10%25",
@@ -82,5 +103,32 @@ public class URIManipulationFilterExtens
         );
     }
 
+    @Test
+    public void testOpaqueUris() {
+        // see SLING_7000
+        assertEquals(
+                "mailto:[email protected]";,
+                underTest.call(renderContext, "mailto:[email protected]";, new 
LinkedHashMap<String, Object>() {{
+                    put(URIManipulationFilterExtension.EXTENSION, "html");
+                }})
+        );
+        // check other opaque links
 
+        // data url according to https://tools.ietf.org/html/rfc2397
+        String dataUrl =
+                "data:image/gif;base64,R0lGODdhMAAwAPAAAAAAAP///ywAAAAAMAAw" +
+                        
"AAAC8IyPqcvt3wCcDkiLc7C0qwyGHhSWpjQu5yqmCYsapyuvUUlvONmOZtfzgFz" +
+                        
"ByTB10QgxOR0TqBQejhRNzOfkVJ+5YiUqrXF5Y5lKh/DeuNcP5yLWGsEbtLiOSp" +
+                        
"a/TPg7JpJHxyendzWTBfX0cxOnKPjgBzi4diinWGdkF8kjdfnycQZXZeYGejmJl" +
+                        
"ZeGl9i2icVqaNVailT6F5iJ90m6mvuTS4OK05M0vDk0Q4XUtwvKOzrcd3iq9uis" +
+                        
"F81M1OIcR7lEewwcLp7tuNNkM3uNna3F2JQFo97Vriy/Xl4/f1cf5VWzXyym7PH" +
+                        "hhx4dbgYKAAA7";
+
+        assertEquals(
+                dataUrl,
+                underTest.call(renderContext, dataUrl, new 
LinkedHashMap<String, Object>() {{
+                    put(URIManipulationFilterExtension.EXTENSION, "html");
+                }})
+        );
+    }
 }

Modified: sling/trunk/bundles/scripting/sightly/testing-content/pom.xml
URL: 
http://svn.apache.org/viewvc/sling/trunk/bundles/scripting/sightly/testing-content/pom.xml?rev=1803550&r1=1803549&r2=1803550&view=diff
==============================================================================
--- sling/trunk/bundles/scripting/sightly/testing-content/pom.xml (original)
+++ sling/trunk/bundles/scripting/sightly/testing-content/pom.xml Mon Jul 31 
16:23:38 2017
@@ -100,7 +100,7 @@
                                 <artifactItem>
                                     <groupId>io.sightly</groupId>
                                     <artifactId>io.sightly.tck</artifactId>
-                                    <version>1.3.4</version>
+                                    <version>1.3.5</version>
                                     <type>jar</type>
                                     
<outputDirectory>${project.build.directory}/sightlytck/</outputDirectory>
                                     
<includes>**/*.html,**/*.js,**/*.java</includes>

Modified: sling/trunk/bundles/scripting/sightly/testing/pom.xml
URL: 
http://svn.apache.org/viewvc/sling/trunk/bundles/scripting/sightly/testing/pom.xml?rev=1803550&r1=1803549&r2=1803550&view=diff
==============================================================================
--- sling/trunk/bundles/scripting/sightly/testing/pom.xml (original)
+++ sling/trunk/bundles/scripting/sightly/testing/pom.xml Mon Jul 31 16:23:38 
2017
@@ -392,7 +392,7 @@
         <dependency>
             <groupId>io.sightly</groupId>
             <artifactId>io.sightly.tck</artifactId>
-            <version>1.3.4</version>
+            <version>1.3.5</version>
             <scope>test</scope>
             <exclusions>
                 <exclusion>


Reply via email to