/******************************************************************************* * 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 java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang.StringUtils; 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; @Component( service = RuntimeExtension.class, property = { RuntimeExtension.NAME + "=" + RuntimeFunction.URI_MANIPULATION } ) public class URIManipulationFilterExtension implements RuntimeExtension { public static final String SCHEME = "scheme"; public static final String DOMAIN = "domain"; public static final String PATH = "path"; public static final String APPEND_PATH = "appendPath"; public static final String PREPEND_PATH = "prependPath"; public static final String SELECTORS = "selectors"; public static final String ADD_SELECTORS = "addSelectors"; public static final String REMOVE_SELECTORS = "removeSelectors"; public static final String EXTENSION = "extension"; public static final String SUFFIX = "suffix"; public static final String PREPEND_SUFFIX = "prependSuffix"; public static final String APPEND_SUFFIX = "appendSuffix"; public static final String FRAGMENT = "fragment"; public static final String QUERY = "query"; public static final String ADD_QUERY = "addQuery"; public static final String REMOVE_QUERY = "removeQuery"; @Override @SuppressWarnings("unchecked") public Object call(RenderContext renderContext, Object... arguments) { ExtensionUtils.checkArgumentCount(RuntimeFunction.URI_MANIPULATION, arguments, 2); 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 += "/"; } } 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); } 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("/")) { 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("&"); } } // 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); } else { if (StringUtils.isNotEmpty(defaultValue)) { stringBuilder.append(defaultValue); } } } private String getOption(String option, Map<String, Object> options, String defaultValue) { if (options.containsKey(option)) { return (String) options.get(option); } return defaultValue; } private void handleSelectors(RuntimeObjectModel runtimeObjectModel, Set<String> selectors, Map<String, Object> options) { if (options.containsKey(SELECTORS)) { Object selectorsOption = options.get(SELECTORS); if (selectorsOption == null) { // we want to remove all selectors selectors.clear(); } else if (selectorsOption instanceof String) { String selectorString = (String) selectorsOption; String[] selectorsArray = selectorString.split("\\."); replaceSelectors(selectors, selectorsArray); } else if (selectorsOption instanceof Object[]) { Object[] selectorsURIArray = (Object[]) selectorsOption; String[] selectorsArray = new String[selectorsURIArray.length]; int index = 0; for (Object selector : selectorsURIArray) { selectorsArray[index++] = runtimeObjectModel.toString(selector); } replaceSelectors(selectors, selectorsArray); } } Object addSelectorsOption = options.get(ADD_SELECTORS); if (addSelectorsOption instanceof String) { String selectorString = (String) addSelectorsOption; String[] selectorsArray = selectorString.split("\\."); addSelectors(selectors, selectorsArray); } else if (addSelectorsOption instanceof Object[]) { Object[] selectorsURIArray = (Object[]) addSelectorsOption; String[] selectorsArray = new String[selectorsURIArray.length]; int index = 0; for (Object selector : selectorsURIArray) { selectorsArray[index++] = runtimeObjectModel.toString(selector); } addSelectors(selectors, selectorsArray); } Object removeSelectorsOption = options.get(REMOVE_SELECTORS); if (removeSelectorsOption instanceof String) { String selectorString = (String) removeSelectorsOption; String[] selectorsArray = selectorString.split("\\."); removeSelectors(selectors, selectorsArray); } else if (removeSelectorsOption instanceof Object[]) { Object[] selectorsURIArray = (Object[]) removeSelectorsOption; String[] selectorsArray = new String[selectorsURIArray.length]; int index = 0; for (Object selector : selectorsURIArray) { selectorsArray[index++] = runtimeObjectModel.toString(selector); } removeSelectors(selectors, 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) { if (options.containsKey(QUERY)) { Object queryOption = options.get(QUERY); parameters.clear(); Map<String, Object> queryParameters = runtimeObjectModel.toMap(queryOption); addQueryParameters(runtimeObjectModel, parameters, queryParameters); } Object addQueryOption = options.get(ADD_QUERY); if (addQueryOption != null) { Map<String, Object> addParams = runtimeObjectModel.toMap(addQueryOption); addQueryParameters(runtimeObjectModel, parameters, addParams); } Object removeQueryOption = options.get(REMOVE_QUERY); if (removeQueryOption != null) { if (removeQueryOption instanceof String) { parameters.remove(removeQueryOption); } else if (removeQueryOption instanceof Object[]) { Object[] removeQueryParamArray = (Object[]) removeQueryOption; for (Object param : removeQueryParamArray) { String paramString = runtimeObjectModel.toString(param); if (paramString != null) { parameters.remove(paramString); } } } } } 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); } } catch (UnsupportedEncodingException e) { throw new SightlyException(e); } } } public static class PathInfo { private URI uri; private String path; private Set<String> selectors; private String selectorString; private String extension; private String suffix; private Map<String, Collection<String>> parameters = new LinkedHashMap<>(); /** * Creates a {@code PathInfo} object based on a request path. * * @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) { 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; } else { this.path = processingPath.substring(0, pathLength); } 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); } } } } /** * 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(); } /** * 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(); } /** * 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(); } /** * Returns the path from which <i>{@code this}</i> object was built. * * @return the original path */ public String getFullPath() { return uri.toString(); } /** * Returns the path identifying the resource, without any selectors, extension or query parameters. * * @return the path of the resource */ public String getPath() { return path; } /** * Returns the selectors set. * * @return the selectors set; if there are no selectors the set will be empty */ public Set<String> getSelectors() { return selectors; } /** * Returns the extension. * * @return the extension, if one exists, otherwise {@code null} */ public String getExtension() { return extension; } /** * Returns the selector string. * * @return the selector string, if the path has selectors, otherwise {@code null} */ public String getSelectorString() { return selectorString; } /** * 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 */ 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(); } /** * 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; } } }