Java tutorial
/* * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.xwiki.url; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLDecoder; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.xwiki.resource.CreateResourceReferenceException; import org.xwiki.velocity.tools.EscapeTool; /** * Extend a {@link URL} by providing access to the URL path segments (URL-decoded). * * @version $Id: 127514ccdae948124d4a3615f04509083c508742 $ * @since 6.1M2 */ public class ExtendedURL implements Cloneable { /** * URL path separator character. */ private static final String URL_SEPARATOR = "/"; private static final String UTF8 = "UTF-8"; /** * @see #getURI() */ private URI uri; /** * Keep the URL that we're wrapping since for some operations it's better to make them on the URL object rather * than on the URI one. For example domain names are stricter in URI (no "_" character allowed while they are * allowed in URLs). * * @see #getWrappedURL() */ private URL wrappedURL; /** * @see #getSegments() */ private List<String> segments; private Map<String, List<String>> parameters; /** * Used to serialize a {@link Map} as a URL query string. */ private EscapeTool escapeTool = new EscapeTool(); /** * Populate the Extended URL with a list of path segments. * * @param segments the path segments of the URL */ public ExtendedURL(List<String> segments) { this(segments, Collections.<String, List<String>>emptyMap()); } /** * Populate the Extended URL with a list of path segments. * * @param segments the path segments of the URL * @param parameters the query string parameters of the URL * @since 7.1M1 */ public ExtendedURL(List<String> segments, Map<String, List<String>> parameters) { this.segments = new ArrayList<>(segments); this.parameters = parameters; } /** * @param url the URL being wrapped * @param ignorePrefix the ignore prefix must start with "/" (eg "/xwiki"). It can be empty or null too in which * case it's not used * @throws org.xwiki.resource.CreateResourceReferenceException if the passed URL is invalid which can happen if it * has incorrect encoding */ public ExtendedURL(URL url, String ignorePrefix) throws CreateResourceReferenceException { this.wrappedURL = url; // Convert the URL to a URI since URI performs correctly decoding. // Note that this means that this method only accepts valid URLs (with proper encoding) URI internalURI; try { internalURI = url.toURI(); } catch (URISyntaxException e) { throw new CreateResourceReferenceException(String.format("Invalid URL [%s]", url), e); } this.uri = internalURI; // Extract the path after the ignore prefix String rawPath = getURI().getRawPath(); if (!StringUtils.isEmpty(ignorePrefix)) { // Allow the passed ignore prefix to not contain the leading "/" String normalizedIgnorePrefix = ignorePrefix; if (!ignorePrefix.startsWith(URL_SEPARATOR)) { normalizedIgnorePrefix = URL_SEPARATOR + ignorePrefix; } if (!getURI().getPath().startsWith(normalizedIgnorePrefix)) { throw new CreateResourceReferenceException( String.format("URL Path [%s] doesn't start with [%s]", getURI().getPath(), ignorePrefix)); } rawPath = rawPath.substring(normalizedIgnorePrefix.length()); } // Remove leading "/" if any rawPath = StringUtils.removeStart(rawPath, URL_SEPARATOR); this.segments = extractPathSegments(rawPath); this.parameters = extractParameters(internalURI); } /** * @return the path segments (each part of the URL separated by the path separator character) */ public List<String> getSegments() { return this.segments; } /** * @return the URL that this instance wraps, provided as a helper feature */ public URL getWrappedURL() { return this.wrappedURL; } /** * @return the URI corresponding to the passed URL that this instance wraps, provided as a helper feature */ public URI getURI() { return this.uri; } /** * @return the list of query string parameters passed in the original URL * @since 7.1M1 */ public Map<String, List<String>> getParameters() { return this.parameters; } private Map<String, List<String>> extractParameters(URI uri) { Map<String, List<String>> uriParameters; if (uri.getQuery() != null) { uriParameters = new LinkedHashMap<>(); for (String nameValue : Arrays.asList(uri.getQuery().split("&"))) { String[] pair = nameValue.split("=", 2); // Check if the parameter has a value or not. if (pair.length == 2) { addParameter(pair[0], pair[1], uriParameters); } else { addParameter(pair[0], null, uriParameters); } } } else { uriParameters = Collections.emptyMap(); } return uriParameters; } private void addParameter(String name, String value, Map<String, List<String>> parameters) { List<String> list = parameters.get(name); if (list == null) { list = new ArrayList<>(); } if (value != null) { list.add(value); } parameters.put(name, list); } /** * Extract segments between "/" characters in the passed path. Also remove any path parameters (i.e. content * after ";" in a path segment; for ex ";jsessionid=...") since we don't want to have these params in the * segments we return and act on (otherwise we would get them in document names for example). * <p> * Note that we only remove ";" characters when they are not URL-encoded. We want to allow the ";" character to be * in document names for example. * * @param rawPath the path from which to extract the segments * @return the extracted path segments */ private List<String> extractPathSegments(String rawPath) { List<String> urlSegments = new ArrayList<String>(); if (StringUtils.isEmpty(rawPath)) { return urlSegments; } // Note that we use -1 in the call below in order to get empty segments too. This is needed since in our URL // scheme a tailing "/" can have a meaning (for example "bin/view/Page" can represent a Page while // "bin/view/Space/" can represents a Space). for (String pathSegment : rawPath.split(URL_SEPARATOR, -1)) { // Remove path parameters String normalizedPathSegment = pathSegment.split(";", 2)[0]; // Now let's decode it String decodedPathSegment; try { // Note: we decode using UTF-8 since the URI javadoc says: // "A sequence of escaped octets is decoded by replacing it with the sequence of characters that it // represents in the UTF-8 character set. UTF-8 contains US-ASCII, hence decoding has the effect of // de-quoting any quoted US-ASCII characters as well as that of decoding any encoded non-US-ASCII // characters." decodedPathSegment = URLDecoder.decode(normalizedPathSegment, UTF8); } catch (UnsupportedEncodingException e) { // Not supporting UTF-8 as a valid encoding for some reasons. We consider XWiki cannot work // without that encoding. throw new RuntimeException( String.format("Failed to URL decode [%s] using UTF-8.", normalizedPathSegment), e); } urlSegments.add(decodedPathSegment); } return urlSegments; } @Override public int hashCode() { return new HashCodeBuilder(7, 7).append(getURI()).append(getSegments()).toHashCode(); } @Override public boolean equals(Object object) { if (object == null) { return false; } if (object == this) { return true; } if (object.getClass() != getClass()) { return false; } ExtendedURL rhs = (ExtendedURL) object; return new EqualsBuilder().append(getURI(), rhs.getURI()).append(getSegments(), rhs.getSegments()) .isEquals(); } /** * @return the serialized segments as a relative URL with URL-encoded path segments. Note that the returned String * starts with a URL separator ("/") */ public String serialize() { StringBuilder builder = new StringBuilder(); List<String> encodedSegments = new ArrayList<>(); for (String path : getSegments()) { encodedSegments.add(encodeSegment(path)); } builder.append(URL_SEPARATOR); builder.append(StringUtils.join(encodedSegments, URL_SEPARATOR)); Map<String, List<String>> uriParameters = getParameters(); if (!uriParameters.isEmpty()) { builder.append('?'); builder.append(this.escapeTool.url(uriParameters)); } return builder.toString(); } private String encodeSegment(String value) { try { return URLEncoder.encode(value, UTF8); } catch (UnsupportedEncodingException e) { // Not supporting UTF-8 as a valid encoding for some reasons. We consider XWiki cannot work // without that encoding. throw new RuntimeException(String.format("Failed to URL encode [%s] using UTF-8.", value), e); } } @Override public String toString() { return serialize(); } }