All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.web.util.HierarchicalUriComponents Maven / Gradle / Ivy

There is a newer version: 6.1.13
Show newest version
/*
 * Copyright 2002-2013 the original author or authors.
 *
 * Licensed 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.springframework.web.util;

import java.io.ByteArrayOutputStream;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

/**
 * Extension of {@link UriComponents} for hierarchical URIs.
 *
 * @author Arjen Poutsma
 * @author Phillip Webb
 * @since 3.1.3
 * @see Hierarchical URIs
 */
@SuppressWarnings("serial")
final class HierarchicalUriComponents extends UriComponents {

	private static final char PATH_DELIMITER = '/';

	private final String userInfo;

	private final String host;

	private final int port;

	private final PathComponent path;

	private final MultiValueMap queryParams;

	private final boolean encoded;


	/**
	 * Package-private constructor. All arguments are optional, and can be {@code null}.
	 * @param scheme the scheme
	 * @param userInfo the user info
	 * @param host the host
	 * @param port the port
	 * @param path the path
	 * @param queryParams the query parameters
	 * @param fragment the fragment
	 * @param encoded whether the components are already encoded
	 * @param verify whether the components need to be checked for illegal characters
	 */
	HierarchicalUriComponents(String scheme, String userInfo, String host, int port, PathComponent path,
			MultiValueMap queryParams, String fragment, boolean encoded, boolean verify) {

		super(scheme, fragment);
		this.userInfo = userInfo;
		this.host = host;
		this.port = port;
		this.path = path != null ? path : NULL_PATH_COMPONENT;
		this.queryParams = CollectionUtils.unmodifiableMultiValueMap(
				queryParams != null ? queryParams : new LinkedMultiValueMap(0));
		this.encoded = encoded;
		if (verify) {
			verify();
		}
	}


	// component getters

	@Override
	public String getSchemeSpecificPart() {
		return null;
	}

	@Override
	public String getUserInfo() {
		return this.userInfo;
	}

	@Override
	public String getHost() {
		return this.host;
	}

	@Override
	public int getPort() {
		return this.port;
	}

	@Override
	public String getPath() {
		return this.path.getPath();
	}

	@Override
	public List getPathSegments() {
		return this.path.getPathSegments();
	}

	@Override
	public String getQuery() {
		if (!this.queryParams.isEmpty()) {
			StringBuilder queryBuilder = new StringBuilder();
			for (Map.Entry> entry : this.queryParams.entrySet()) {
				String name = entry.getKey();
				List values = entry.getValue();
				if (CollectionUtils.isEmpty(values)) {
					if (queryBuilder.length() != 0) {
						queryBuilder.append('&');
					}
					queryBuilder.append(name);
				}
				else {
					for (Object value : values) {
						if (queryBuilder.length() != 0) {
							queryBuilder.append('&');
						}
						queryBuilder.append(name);

						if (value != null) {
							queryBuilder.append('=');
							queryBuilder.append(value.toString());
						}
					}
				}
			}
			return queryBuilder.toString();
		}
		else {
			return null;
		}
	}

	/**
	 * Returns the map of query parameters. Empty if no query has been set.
	 */
	@Override
	public MultiValueMap getQueryParams() {
		return this.queryParams;
	}


	// encoding

	/**
	 * Encodes all URI components using their specific encoding rules, and returns the result as a new
	 * {@code UriComponents} instance.
	 * @param encoding the encoding of the values contained in this map
	 * @return the encoded uri components
	 * @throws UnsupportedEncodingException if the given encoding is not supported
	 */
	@Override
	public HierarchicalUriComponents encode(String encoding) throws UnsupportedEncodingException {
		Assert.hasLength(encoding, "'encoding' must not be empty");
		if (this.encoded) {
			return this;
		}
		String encodedScheme = encodeUriComponent(this.getScheme(), encoding, Type.SCHEME);
		String encodedUserInfo = encodeUriComponent(this.userInfo, encoding, Type.USER_INFO);
		String encodedHost = encodeUriComponent(this.host, encoding, Type.HOST);
		PathComponent encodedPath = this.path.encode(encoding);
		MultiValueMap encodedQueryParams =
				new LinkedMultiValueMap(this.queryParams.size());
		for (Map.Entry> entry : this.queryParams.entrySet()) {
			String encodedName = encodeUriComponent(entry.getKey(), encoding, Type.QUERY_PARAM);
			List encodedValues = new ArrayList(entry.getValue().size());
			for (String value : entry.getValue()) {
				String encodedValue = encodeUriComponent(value, encoding, Type.QUERY_PARAM);
				encodedValues.add(encodedValue);
			}
			encodedQueryParams.put(encodedName, encodedValues);
		}
		String encodedFragment = encodeUriComponent(this.getFragment(), encoding, Type.FRAGMENT);
		return new HierarchicalUriComponents(encodedScheme, encodedUserInfo, encodedHost, this.port, encodedPath,
				encodedQueryParams, encodedFragment, true, false);
	}

	/**
	 * Encodes the given source into an encoded String using the rules specified
	 * by the given component and with the given options.
	 * @param source the source string
	 * @param encoding the encoding of the source string
	 * @param type the URI component for the source
	 * @return the encoded URI
	 * @throws IllegalArgumentException when the given uri parameter is not a valid URI
	 */
	static String encodeUriComponent(String source, String encoding, Type type) throws UnsupportedEncodingException {
		if (source == null) {
			return null;
		}
		Assert.hasLength(encoding, "'encoding' must not be empty");
		byte[] bytes = encodeBytes(source.getBytes(encoding), type);
		return new String(bytes, "US-ASCII");
	}

	private static byte[] encodeBytes(byte[] source, Type type) {
		Assert.notNull(source, "'source' must not be null");
		Assert.notNull(type, "'type' must not be null");
		ByteArrayOutputStream bos = new ByteArrayOutputStream(source.length);
		for (byte b : source) {
			if (b < 0) {
				b += 256;
			}
			if (type.isAllowed(b)) {
				bos.write(b);
			}
			else {
				bos.write('%');
				char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, 16));
				char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, 16));
				bos.write(hex1);
				bos.write(hex2);
			}
		}
		return bos.toByteArray();
	}


	// verifying

	/**
	 * Verifies all URI components to determine whether they contain any illegal
	 * characters, throwing an {@code IllegalArgumentException} if so.
	 * @throws IllegalArgumentException if any component has illegal characters
	 */
	private void verify() {
		if (!this.encoded) {
			return;
		}
		verifyUriComponent(getScheme(), Type.SCHEME);
		verifyUriComponent(userInfo, Type.USER_INFO);
		verifyUriComponent(host, Type.HOST);
		this.path.verify();
		for (Map.Entry> entry : queryParams.entrySet()) {
			verifyUriComponent(entry.getKey(), Type.QUERY_PARAM);
			for (String value : entry.getValue()) {
				verifyUriComponent(value, Type.QUERY_PARAM);
			}
		}
		verifyUriComponent(getFragment(), Type.FRAGMENT);
	}

	private static void verifyUriComponent(String source, Type type) {
		if (source == null) {
			return;
		}
		int length = source.length();
		for (int i=0; i < length; i++) {
			char ch = source.charAt(i);
			if (ch == '%') {
				if ((i + 2) < length) {
					char hex1 = source.charAt(i + 1);
					char hex2 = source.charAt(i + 2);
					int u = Character.digit(hex1, 16);
					int l = Character.digit(hex2, 16);
					if (u == -1 || l == -1) {
						throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\"");
					}
					i += 2;
				}
				else {
					throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\"");
				}
			}
			else if (!type.isAllowed(ch)) {
				throw new IllegalArgumentException(
						"Invalid character '" + ch + "' for " + type.name() + " in \"" + source + "\"");
			}
		}
	}


	// expanding

	@Override
	protected HierarchicalUriComponents expandInternal(UriTemplateVariables uriVariables) {
		Assert.state(!this.encoded, "Cannot expand an already encoded UriComponents object");
		String expandedScheme = expandUriComponent(this.getScheme(), uriVariables);
		String expandedUserInfo = expandUriComponent(this.userInfo, uriVariables);
		String expandedHost = expandUriComponent(this.host, uriVariables);
		PathComponent expandedPath = this.path.expand(uriVariables);
		MultiValueMap expandedQueryParams =
				new LinkedMultiValueMap(this.queryParams.size());
		for (Map.Entry> entry : this.queryParams.entrySet()) {
			String expandedName = expandUriComponent(entry.getKey(), uriVariables);
			List expandedValues = new ArrayList(entry.getValue().size());
			for (String value : entry.getValue()) {
				String expandedValue = expandUriComponent(value, uriVariables);
				expandedValues.add(expandedValue);
			}
			expandedQueryParams.put(expandedName, expandedValues);
		}
		String expandedFragment = expandUriComponent(this.getFragment(), uriVariables);
		return new HierarchicalUriComponents(expandedScheme, expandedUserInfo, expandedHost, this.port, expandedPath,
				expandedQueryParams, expandedFragment, false, false);
	}

	/**
	 * Normalize the path removing sequences like "path/..".
	 * @see StringUtils#cleanPath(String)
	 */
	@Override
	public UriComponents normalize() {
		String normalizedPath = StringUtils.cleanPath(getPath());
		return new HierarchicalUriComponents(getScheme(), this.userInfo, this.host,
				this.port, new FullPathComponent(normalizedPath), this.queryParams,
				getFragment(), this.encoded, false);
	}


	// other functionality

	/**
	 * Returns a URI string from this {@code UriComponents} instance.
	 */
	@Override
	public String toUriString() {
		StringBuilder uriBuilder = new StringBuilder();
		if (getScheme() != null) {
			uriBuilder.append(getScheme());
			uriBuilder.append(':');
		}
		if (this.userInfo != null || this.host != null) {
			uriBuilder.append("//");
			if (this.userInfo != null) {
				uriBuilder.append(this.userInfo);
				uriBuilder.append('@');
			}
			if (this.host != null) {
				uriBuilder.append(host);
			}
			if (this.port != -1) {
				uriBuilder.append(':');
				uriBuilder.append(port);
			}
		}
		String path = getPath();
		if (StringUtils.hasLength(path)) {
			if (uriBuilder.length() != 0 && path.charAt(0) != PATH_DELIMITER) {
				uriBuilder.append(PATH_DELIMITER);
			}
			uriBuilder.append(path);
		}
		String query = getQuery();
		if (query != null) {
			uriBuilder.append('?');
			uriBuilder.append(query);
		}
		if (getFragment() != null) {
			uriBuilder.append('#');
			uriBuilder.append(getFragment());
		}
		return uriBuilder.toString();
	}

	/**
	 * Returns a {@code URI} from this {@code UriComponents} instance.
	 */
	@Override
	public URI toUri() {
		try {
			if (this.encoded) {
				return new URI(toString());
			}
			else {
				String path = getPath();
				if (StringUtils.hasLength(path) && path.charAt(0) != PATH_DELIMITER) {
					// Only prefix the path delimiter if something exists before it
					if(getScheme() != null || getUserInfo() != null || getHost() != null || getPort() != -1) {
						path = PATH_DELIMITER + path;
					}
				}
				return new URI(getScheme(), getUserInfo(), getHost(), getPort(), path, getQuery(),
						getFragment());
			}
		}
		catch (URISyntaxException ex) {
			throw new IllegalStateException("Could not create URI object: " + ex.getMessage(), ex);
		}
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (!(obj instanceof HierarchicalUriComponents)) {
			return false;
		}
		HierarchicalUriComponents other = (HierarchicalUriComponents) obj;
		return ObjectUtils.nullSafeEquals(getScheme(), other.getScheme()) &&
				ObjectUtils.nullSafeEquals(getUserInfo(), other.getUserInfo()) &&
				ObjectUtils.nullSafeEquals(getHost(), other.getHost()) &&
				getPort() == other.getPort() &&
				this.path.equals(other.path) &&
				this.queryParams.equals(other.queryParams) &&
				ObjectUtils.nullSafeEquals(getFragment(), other.getFragment());
	}

	@Override
	public int hashCode() {
		int result = ObjectUtils.nullSafeHashCode(getScheme());
		result = 31 * result + ObjectUtils.nullSafeHashCode(this.userInfo);
		result = 31 * result + ObjectUtils.nullSafeHashCode(this.host);
		result = 31 * result + this.port;
		result = 31 * result + this.path.hashCode();
		result = 31 * result + this.queryParams.hashCode();
		result = 31 * result + ObjectUtils.nullSafeHashCode(getFragment());
		return result;
	}


	// inner types

	/**
	 * Enumeration used to identify the parts of a URI.
	 * 

Contains methods to indicate whether a given character is valid in a specific URI component. * @see RFC 3986 */ static enum Type { SCHEME { @Override public boolean isAllowed(int c) { return isAlpha(c) || isDigit(c) || '+' == c || '-' == c || '.' == c; } }, AUTHORITY { @Override public boolean isAllowed(int c) { return isUnreserved(c) || isSubDelimiter(c) || ':' == c || '@' == c; } }, USER_INFO { @Override public boolean isAllowed(int c) { return isUnreserved(c) || isSubDelimiter(c) || ':' == c; } }, HOST { @Override public boolean isAllowed(int c) { return isUnreserved(c) || isSubDelimiter(c); } }, PORT { @Override public boolean isAllowed(int c) { return isDigit(c); } }, PATH { @Override public boolean isAllowed(int c) { return isPchar(c) || '/' == c; } }, PATH_SEGMENT { @Override public boolean isAllowed(int c) { return isPchar(c); } }, QUERY { @Override public boolean isAllowed(int c) { return isPchar(c) || '/' == c || '?' == c; } }, QUERY_PARAM { @Override public boolean isAllowed(int c) { if ('=' == c || '+' == c || '&' == c) { return false; } else { return isPchar(c) || '/' == c || '?' == c; } } }, FRAGMENT { @Override public boolean isAllowed(int c) { return isPchar(c) || '/' == c || '?' == c; } }; /** * Indicates whether the given character is allowed in this URI component. * @return {@code true} if the character is allowed; {@code false} otherwise */ public abstract boolean isAllowed(int c); /** * Indicates whether the given character is in the {@code ALPHA} set. * @see RFC 3986, appendix A */ protected boolean isAlpha(int c) { return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'; } /** * Indicates whether the given character is in the {@code DIGIT} set. * @see RFC 3986, appendix A */ protected boolean isDigit(int c) { return c >= '0' && c <= '9'; } /** * Indicates whether the given character is in the {@code gen-delims} set. * @see RFC 3986, appendix A */ protected boolean isGenericDelimiter(int c) { return ':' == c || '/' == c || '?' == c || '#' == c || '[' == c || ']' == c || '@' == c; } /** * Indicates whether the given character is in the {@code sub-delims} set. * @see RFC 3986, appendix A */ protected boolean isSubDelimiter(int c) { return '!' == c || '$' == c || '&' == c || '\'' == c || '(' == c || ')' == c || '*' == c || '+' == c || ',' == c || ';' == c || '=' == c; } /** * Indicates whether the given character is in the {@code reserved} set. * @see RFC 3986, appendix A */ protected boolean isReserved(char c) { return isGenericDelimiter(c) || isReserved(c); } /** * Indicates whether the given character is in the {@code unreserved} set. * @see RFC 3986, appendix A */ protected boolean isUnreserved(int c) { return isAlpha(c) || isDigit(c) || '-' == c || '.' == c || '_' == c || '~' == c; } /** * Indicates whether the given character is in the {@code pchar} set. * @see RFC 3986, appendix A */ protected boolean isPchar(int c) { return isUnreserved(c) || isSubDelimiter(c) || ':' == c || '@' == c; } } /** * Defines the contract for path (segments). */ interface PathComponent extends Serializable { String getPath(); List getPathSegments(); PathComponent encode(String encoding) throws UnsupportedEncodingException; void verify(); PathComponent expand(UriTemplateVariables uriVariables); } /** * Represents a path backed by a string. */ static final class FullPathComponent implements PathComponent { private final String path; public FullPathComponent(String path) { this.path = path; } public String getPath() { return path; } public List getPathSegments() { String delimiter = new String(new char[]{PATH_DELIMITER}); String[] pathSegments = StringUtils.tokenizeToStringArray(path, delimiter); return Collections.unmodifiableList(Arrays.asList(pathSegments)); } public PathComponent encode(String encoding) throws UnsupportedEncodingException { String encodedPath = encodeUriComponent(getPath(),encoding, Type.PATH); return new FullPathComponent(encodedPath); } public void verify() { verifyUriComponent(this.path, Type.PATH); } public PathComponent expand(UriTemplateVariables uriVariables) { String expandedPath = expandUriComponent(getPath(), uriVariables); return new FullPathComponent(expandedPath); } @Override public boolean equals(Object obj) { return (this == obj || (obj instanceof FullPathComponent && getPath().equals(((FullPathComponent) obj).getPath()))); } @Override public int hashCode() { return getPath().hashCode(); } } /** * Represents a path backed by a string list (i.e. path segments). */ static final class PathSegmentComponent implements PathComponent { private final List pathSegments; public PathSegmentComponent(List pathSegments) { this.pathSegments = Collections.unmodifiableList(pathSegments); } public String getPath() { StringBuilder pathBuilder = new StringBuilder(); pathBuilder.append(PATH_DELIMITER); for (Iterator iterator = this.pathSegments.iterator(); iterator.hasNext(); ) { String pathSegment = iterator.next(); pathBuilder.append(pathSegment); if (iterator.hasNext()) { pathBuilder.append(PATH_DELIMITER); } } return pathBuilder.toString(); } public List getPathSegments() { return this.pathSegments; } public PathComponent encode(String encoding) throws UnsupportedEncodingException { List pathSegments = getPathSegments(); List encodedPathSegments = new ArrayList(pathSegments.size()); for (String pathSegment : pathSegments) { String encodedPathSegment = encodeUriComponent(pathSegment, encoding, Type.PATH_SEGMENT); encodedPathSegments.add(encodedPathSegment); } return new PathSegmentComponent(encodedPathSegments); } public void verify() { for (String pathSegment : getPathSegments()) { verifyUriComponent(pathSegment, Type.PATH_SEGMENT); } } public PathComponent expand(UriTemplateVariables uriVariables) { List pathSegments = getPathSegments(); List expandedPathSegments = new ArrayList(pathSegments.size()); for (String pathSegment : pathSegments) { String expandedPathSegment = expandUriComponent(pathSegment, uriVariables); expandedPathSegments.add(expandedPathSegment); } return new PathSegmentComponent(expandedPathSegments); } @Override public boolean equals(Object obj) { return (this == obj || (obj instanceof PathSegmentComponent && getPathSegments().equals(((PathSegmentComponent) obj).getPathSegments()))); } @Override public int hashCode() { return getPathSegments().hashCode(); } } /** * Represents a collection of PathComponents. */ static final class PathComponentComposite implements PathComponent { private final List pathComponents; public PathComponentComposite(List pathComponents) { this.pathComponents = pathComponents; } public String getPath() { StringBuilder pathBuilder = new StringBuilder(); for (PathComponent pathComponent : this.pathComponents) { pathBuilder.append(pathComponent.getPath()); } return pathBuilder.toString(); } public List getPathSegments() { List result = new ArrayList(); for (PathComponent pathComponent : this.pathComponents) { result.addAll(pathComponent.getPathSegments()); } return result; } public PathComponent encode(String encoding) throws UnsupportedEncodingException { List encodedComponents = new ArrayList(this.pathComponents.size()); for (PathComponent pathComponent : this.pathComponents) { encodedComponents.add(pathComponent.encode(encoding)); } return new PathComponentComposite(encodedComponents); } public void verify() { for (PathComponent pathComponent : this.pathComponents) { pathComponent.verify(); } } public PathComponent expand(UriTemplateVariables uriVariables) { List expandedComponents = new ArrayList(this.pathComponents.size()); for (PathComponent pathComponent : this.pathComponents) { expandedComponents.add(pathComponent.expand(uriVariables)); } return new PathComponentComposite(expandedComponents); } } /** * Represents an empty path. */ static final PathComponent NULL_PATH_COMPONENT = new PathComponent() { public String getPath() { return null; } public List getPathSegments() { return Collections.emptyList(); } public PathComponent encode(String encoding) throws UnsupportedEncodingException { return this; } public void verify() { } public PathComponent expand(UriTemplateVariables uriVariables) { return this; } @Override public boolean equals(Object obj) { return (this == obj); } @Override public int hashCode() { return 42; } }; }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy