/** * Copyright 2005-2014 Restlet * * The contents of this file are subject to the terms of one of the following * open source licenses: Apache 2.0 or or EPL 1.0 (the "Licenses"). You can * select the license that you prefer but you may not use this file except in * compliance with one of these Licenses. * * You can obtain a copy of the Apache 2.0 license at * http://www.opensource.org/licenses/apache-2.0 * * You can obtain a copy of the EPL 1.0 license at * http://www.opensource.org/licenses/eclipse-1.0 * * See the Licenses for the specific language governing permissions and * limitations under the Licenses. * * Alternatively, you can obtain a royalty free commercial license with less * limitations, transferable or non-transferable, directly at * http://restlet.com/products/restlet-framework * * Restlet is a registered trademark of Restlet S.A.S. */ package org.restlet.data; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import org.restlet.Context; import org.restlet.engine.Edition; /** * Reference to a Uniform Resource Identifier (URI). Contrary to the * java.net.URI class, this interface represents mutable references. It strictly * conforms to the RFC 3986 specifying URIs and follow its naming conventions.<br> * * <pre> * URI reference = absolute-reference | relative-reference * * absolute-reference = scheme ":" scheme-specific-part [ "#" fragment ] * scheme-specific-part = ( hierarchical-part [ "?" query ] ) | opaque-part * hierarchical-part = ( "//" authority path-abempty ) | path-absolute | path-rootless | path-empty * authority = [ user-info "@" ] host-domain [ ":" host-port ] * * relative-reference = relative-part [ "?" query ] [ "#" fragment ] * relative-part = ( "//" authority path-abempty ) | path-absolute | path-noscheme | path-empty * * path-abempty = begins with "/" or is empty * path-absolute = begins with "/" but not "//" * path-noscheme = begins with a non-colon segment * path-rootless = begins with a segment * path-empty = zero characters * </pre> * * <p> * Note that this class doesn't encode or decode the reserved characters. It * assumes that the URIs or the URI parts passed in are properly encoded using * the standard URI encoding mechanism. You can use the static "encode()" and * "decode()" methods for this purpose. Note that if an invalid URI character is * detected by the constructor or one of the setters, a trace will be logged and * the character will be automatically encoded. * </p> * <p> * The fundamental point to underline is the difference between an URI * "reference" and an URI. Contrary to an URI (the target identifier of a REST * resource), an URI reference can be relative (with or without query and * fragment part). This relative URI reference can then be resolved against a * base reference via the getTargetRef() method which will return a new resolved * Reference instance, an absolute URI reference with no base reference and with * no dot-segments (the path segments "." and ".."). * </p> * <p> * You can also apply the getTargetRef() method on absolute references in order * to solve the dot-segments. Note that applying the getRelativeRef() method on * an absolute reference returns the current reference relatively to a base * reference, if any, and solves the dot-segments. * </p> * <p> * The Reference stores its data as a single string, the one passed to the * constructor. This string can always be obtained using the toString() method. * A couple of integer indexes are maintained to improve the extraction time of * various reference properties (URI components). * </p> * <p> * When you modify a specific component of the URI reference, via the setPath() * method for example, the internal string is simply regenerated by updating * only the relevant part. We try as much as possible to protect the bytes given * to the Reference class instead of transparently parsing and normalizing the * URI data. Our idea is to protect encodings and special characters in all case * and reduce the memory size taken by this class while making Reference * instances mutable. * </p> * <p> * Because the base reference is only a property of the Reference ("baseRef"). * When you use the "Reference(base, path)" constructor, it is equivalent to * doing:<br> * ref = new Reference(path);<br> * ref.setBaseRef(base); * </p> * <p> * The base ref is not automatically resolved or "merged" with the rest of the * reference information (the path here). For example, this let's you reuse a * single reference as the base of several relative references. If you modify * the base reference, all relative references are still accurate. * </p> * Note that the name and value properties are thread safe, stored in volatile * members. * * @author Jerome Louvel * @see <a href="http://tools.ietf.org/html/rfc3986">RFC 3986</a> */ public class Reference { /** Helps to map characters and their validity as URI characters. */ private static final boolean[] charValidityMap = new boolean[127]; static { // Initialize the map of valid characters. for (int character = 0; character < 127; character++) { charValidityMap[character] = isReserved(character) || isUnreserved(character) || (character == '%') || (character == '{') || (character == '}'); } } /** * Decodes a given string using the standard URI encoding mechanism and the * UTF-8 character set. * * @param toDecode * The string to decode. * @return The decoded string. */ public static String decode(String toDecode) { return decode(toDecode, CharacterSet.UTF_8); } /** * Decodes a given string using the standard URI encoding mechanism. If the * provided character set is null, the string is returned but not decoded. * <em><strong>Note:</strong> The <a * href="http://www.w3.org/TR/html40/appendix/notes.html#non-ascii-chars"> * World Wide Web Consortium Recommendation</a> states that UTF-8 should be * used. Not doing so may introduce incompatibilities.</em> * * @param toDecode * The string to decode. * @param characterSet * The name of a supported character encoding. * @return The decoded string or null if the named character encoding is not * supported. */ public static String decode(String toDecode, CharacterSet characterSet) { if (Edition.CURRENT == Edition.GWT) { if (!CharacterSet.UTF_8.equals(characterSet)) { throw new IllegalArgumentException( "Only UTF-8 URL encoding is supported under GWT"); } } String result = null; // [ifndef gwt] try { result = (characterSet == null) ? toDecode : java.net.URLDecoder .decode(toDecode, characterSet.getName()); } catch (UnsupportedEncodingException uee) { Context.getCurrentLogger() .log(Level.WARNING, "Unable to decode the string with the UTF-8 character set.", uee); } // [enddef] // [ifdef gwt] uncomment // try { // result = (characterSet == null) ? toDecode : // com.google.gwt.http.client.URL.decodeComponent(toDecode); // } catch (NullPointerException npe) { // System.err // .println("Unable to decode the string with the UTF-8 character set."); // } // [enddef] return result; } /** * Encodes a given string using the standard URI encoding mechanism and the * UTF-8 character set. * * @param toEncode * The string to encode. * @return The encoded string. */ public static String encode(String toEncode) { return encode(toEncode, true, CharacterSet.UTF_8); } /** * Encodes a given string using the standard URI encoding mechanism and the * UTF-8 character set. Useful to prevent the usage of '+' to encode spaces * (%20 instead). The '*' characters are encoded as %2A and %7E are replaced * by '~'. * * @param toEncode * The string to encode. * @param queryString * True if the string to encode is part of a query string instead * of a HTML form post. * @return The encoded string. */ public static String encode(String toEncode, boolean queryString) { return encode(toEncode, queryString, CharacterSet.UTF_8); } /** * Encodes a given string using the standard URI encoding mechanism and the * UTF-8 character set. Useful to prevent the usage of '+' to encode spaces * (%20 instead). The '*' characters are encoded as %2A and %7E are replaced * by '~'. * * @param toEncode * The string to encode. * @param queryString * True if the string to encode is part of a query string instead * of a HTML form post. * @param characterSet * The supported character encoding. * @return The encoded string. */ public static String encode(String toEncode, boolean queryString, CharacterSet characterSet) { if (Edition.CURRENT == Edition.GWT) { if (!CharacterSet.UTF_8.equals(characterSet)) { throw new IllegalArgumentException( "Only UTF-8 URL encoding is supported under GWT"); } } String result = null; // [ifndef gwt] try { result = (characterSet == null) ? toEncode : java.net.URLEncoder .encode(toEncode, characterSet.getName()); } catch (UnsupportedEncodingException uee) { Context.getCurrentLogger() .log(Level.WARNING, "Unable to encode the string with the UTF-8 character set.", uee); } // [enddef] // [ifdef gwt] uncomment // try { // result = (characterSet == null) ? toEncode : // com.google.gwt.http.client.URL.encodeComponent(toEncode); // } catch (NullPointerException npe) { // System.err // .println("Unable to encode the string with the UTF-8 character set."); // } // [enddef] if (queryString) { result = result.replace("+", "%20").replace("*", "%2A") .replace("%7E", "~"); } return result; } /** * Encodes a given string using the standard URI encoding mechanism. If the * provided character set is null, the string is returned but not encoded. * * <em><strong>Note:</strong> The <a * href="http://www.w3.org/TR/html40/appendix/notes.html#non-ascii-chars"> * World Wide Web Consortium Recommendation</a> states that UTF-8 should be * used. Not doing so may introduce incompatibilities.</em> * * @param toEncode * The string to encode. * @param characterSet * The supported character encoding. * @return The encoded string or null if the named character encoding is not * supported. */ public static String encode(String toEncode, CharacterSet characterSet) { return encode(toEncode, true, characterSet); } /** * Indicates if the given character is alphabetical (a-z or A-Z). * * @param character * The character to test. * @return True if the given character is alphabetical (a-z or A-Z). */ private static boolean isAlpha(int character) { return isUpperCase(character) || isLowerCase(character); } /** * Indicates if the given character is a digit (0-9). * * @param character * The character to test. * @return True if the given character is a digit (0-9). */ private static boolean isDigit(int character) { return (character >= '0') && (character <= '9'); } /** * Indicates if the given character is a generic URI component delimiter * character. * * @param character * The character to test. * @return True if the given character is a generic URI component delimiter * character. */ public static boolean isGenericDelimiter(int character) { return (character == ':') || (character == '/') || (character == '?') || (character == '#') || (character == '[') || (character == ']') || (character == '@'); } /** * Indicates if the given character is lower case (a-z). * * @param character * The character to test. * @return True if the given character is lower case (a-z). */ private static boolean isLowerCase(int character) { return (character >= 'a') && (character <= 'z'); } /** * Indicates if the given character is a reserved URI character. * * @param character * The character to test. * @return True if the given character is a reserved URI character. */ public static boolean isReserved(int character) { return isGenericDelimiter(character) || isSubDelimiter(character); } /** * Indicates if the given character is an URI subcomponent delimiter * character. * * @param character * The character to test. * @return True if the given character is an URI subcomponent delimiter * character. */ public static boolean isSubDelimiter(int character) { return (character == '!') || (character == '$') || (character == '&') || (character == '\'') || (character == '(') || (character == ')') || (character == '*') || (character == '+') || (character == ',') || (character == ';') || (character == '='); } /** * Indicates if the given character is an unreserved URI character. * * @param character * The character to test. * @return True if the given character is an unreserved URI character. */ public static boolean isUnreserved(int character) { return isAlpha(character) || isDigit(character) || (character == '-') || (character == '.') || (character == '_') || (character == '~'); } /** * Indicates if the given character is upper case (A-Z). * * @param character * The character to test. * @return True if the given character is upper case (A-Z). */ private static boolean isUpperCase(int character) { return (character >= 'A') && (character <= 'Z'); } /** * Indicates if the given character is a valid URI character. * * @param character * The character to test. * @return True if the given character is a valid URI character. */ public static boolean isValid(int character) { return character >= 0 && character < 127 && charValidityMap[character]; } /** * Creates a reference string from its parts. * * @param scheme * The scheme ("http", "https" or "ftp"). * @param hostName * The host name or IP address. * @param hostPort * The host port (default ports are correctly ignored). * @param path * The path component for hierarchical identifiers. * @param query * The optional query component for hierarchical identifiers. * @param fragment * The optional fragment identifier. * @return The reference as String. */ public static String toString(String scheme, String hostName, Integer hostPort, String path, String query, String fragment) { String host = hostName; // Appends the host port number if (hostPort != null) { final int defaultPort = Protocol.valueOf(scheme).getDefaultPort(); if (hostPort != defaultPort) { host = hostName + ':' + hostPort; } } return toString(scheme, host, path, query, fragment); } /** * Creates a relative reference string from its parts. * * @param relativePart * The relative part component. * @param query * The optional query component for hierarchical identifiers. * @param fragment * The optional fragment identifier. * @return The relative reference as a String. */ public static String toString(String relativePart, String query, String fragment) { final StringBuilder sb = new StringBuilder(); // Append the path if (relativePart != null) { sb.append(relativePart); } // Append the query string if (query != null) { sb.append('?').append(query); } // Append the fragment identifier if (fragment != null) { sb.append('#').append(fragment); } // Actually construct the reference return sb.toString(); } /** * Creates a reference string from its parts. * * @param scheme * The scheme ("http", "https" or "ftp"). * @param host * The host name or IP address plus the optional port number. * @param path * The path component for hierarchical identifiers. * @param query * The optional query component for hierarchical identifiers. * @param fragment * The optional fragment identifier. * @return The reference a String. */ public static String toString(String scheme, String host, String path, String query, String fragment) { final StringBuilder sb = new StringBuilder(); if (scheme != null) { // Append the scheme and host name sb.append(scheme.toLowerCase()).append("://").append(host); } // Append the path if (path != null) { sb.append(path); } // Append the query string if (query != null) { sb.append('?').append(query); } // Append the fragment identifier if (fragment != null) { sb.append('#').append(fragment); } // Actually construct the reference return sb.toString(); } /** The base reference for relative references. */ private volatile Reference baseRef; /** The fragment separator index. */ private volatile int fragmentIndex; /** The internal reference. */ private volatile String internalRef; /** The query separator index. */ private volatile int queryIndex; /** The scheme separator index. */ private volatile int schemeIndex; /** * Empty constructor. */ public Reference() { this((Reference) null, (String) null); } // [ifndef gwt] method /** * Constructor from an {@link java.net.URI} instance. * * @param uri * The {@link java.net.URI} instance. */ public Reference(java.net.URI uri) { this(uri.toString()); } // [ifndef gwt] method /** * Constructor from an {@link java.net.URI} instance. * * @param baseUri * The base {@link java.net.URI} instance. * @param uri * The {@link java.net.URI} instance. */ public Reference(java.net.URI baseUri, java.net.URI uri) { this(baseUri.toString(), uri.toString()); } // [ifndef gwt] method /** * Constructor from an {@link java.net.URL} instance. * * @param url * The {@link java.net.URL} instance. */ public Reference(java.net.URL url) { this(url.toString()); } /** * Constructor for a protocol and host name. Uses the default port for the * given protocol. * * @param protocol * Protocol/scheme to use * @param hostName * The host name or IP address. */ public Reference(Protocol protocol, String hostName) { this(protocol, hostName, protocol.getDefaultPort()); } /** * Constructor for a protocol, host name and host port * * @param protocol * Protocol/scheme to use * @param hostName * The host name or IP address. * @param hostPort * The host port (default ports are correctly ignored). */ public Reference(Protocol protocol, String hostName, int hostPort) { this(protocol.getSchemeName(), hostName, hostPort, null, null, null); } /** * Clone constructor. * * @param ref * The reference to clone. */ public Reference(Reference ref) { this(ref.baseRef, ref.internalRef); } /** * Constructor from an URI reference (most likely relative). * * @param baseRef * The base reference. * @param uriReference * The URI reference, either absolute or relative. */ public Reference(Reference baseRef, Reference uriReference) { this(baseRef, uriReference.toString()); } /** * Constructor from an URI reference (most likely relative). * * @param baseRef * The base reference. * @param uriRef * The URI reference, either absolute or relative. */ public Reference(Reference baseRef, String uriRef) { uriRef = encodeInvalidCharacters(uriRef); this.baseRef = baseRef; this.internalRef = uriRef; updateIndexes(); } /** * Constructor of relative reference from its parts. * * @param baseRef * The base reference. * @param relativePart * The relative part component (most of the time it is the path * component). * @param query * The optional query component for hierarchical identifiers. * @param fragment * The optional fragment identifier. */ public Reference(Reference baseRef, String relativePart, String query, String fragment) { this(baseRef, toString(relativePart, query, fragment)); } /** * Constructor from an URI reference. * * @param uriReference * The URI reference, either absolute or relative. */ public Reference(String uriReference) { this((Reference) null, uriReference); } /** * Constructor from an identifier and a fragment. * * @param identifier * The resource identifier. * @param fragment * The fragment identifier. */ public Reference(String identifier, String fragment) { this((fragment == null) ? identifier : identifier + '#' + fragment); } /** * Constructor of absolute reference from its parts. * * @param scheme * The scheme ("http", "https" or "ftp"). * @param hostName * The host name or IP address. * @param hostPort * The host port (default ports are correctly ignored). * @param path * The path component for hierarchical identifiers. * @param query * The optional query component for hierarchical identifiers. * @param fragment * The optional fragment identifier. */ public Reference(String scheme, String hostName, int hostPort, String path, String query, String fragment) { this(toString(scheme, hostName, hostPort, path, query, fragment)); } /** * Adds a parameter to the query component. The name and value are * automatically URL encoded if necessary. * * @param parameter * The parameter to add. * @return The updated reference. */ public Reference addQueryParameter(Parameter parameter) { return addQueryParameter(parameter.getName(), parameter.getValue()); } /** * Adds a parameter to the query component. The name and value are * automatically URL encoded if necessary. * * @param name * The parameter name. * @param value * The optional parameter value. * @return The updated reference. */ public Reference addQueryParameter(String name, String value) { String query = getQuery(); if (query == null) { if (value == null) { setQuery(encode(name)); } else { setQuery(encode(name) + '=' + encode(value)); } } else { if (value == null) { setQuery(query + '&' + encode(name)); } else { setQuery(query + '&' + encode(name) + '=' + encode(value)); } } return this; } /** * Adds several parameters to the query component. The name and value are * automatically URL encoded if necessary. * * @param parameters * The parameters to add. * @return The updated reference. */ public Reference addQueryParameters(Iterable<Parameter> parameters) { for (Parameter param : parameters) { addQueryParameter(param); } return this; } /** * Adds a segment at the end of the path. If the current path doesn't end * with a slash character, one is inserted before the new segment value. The * value is automatically encoded if necessary. * * @param value * The segment value to add. * @return The updated reference. */ public Reference addSegment(String value) { final String path = getPath(); if (value != null) { if (path == null) { setPath("/" + value); } else if (path.endsWith("/")) { setPath(path + encode(value)); } else { setPath(path + "/" + encode(value)); } } return this; } // [ifndef gwt] method @Override public Reference clone() { final Reference newRef = new Reference(); if (this.baseRef == null) { newRef.baseRef = null; } else if (equals(this.baseRef)) { newRef.baseRef = newRef; } else { newRef.baseRef = this.baseRef.clone(); } newRef.fragmentIndex = this.fragmentIndex; newRef.internalRef = this.internalRef; newRef.queryIndex = this.queryIndex; newRef.schemeIndex = this.schemeIndex; return newRef; } /** * Checks if all characters are valid and encodes invalid characters if * necessary. * * @param uriRef * The URI reference to check. * @return The original reference, eventually with invalid URI characters * encoded. */ private String encodeInvalidCharacters(String uriRef) throws IllegalArgumentException { String result = uriRef; if (uriRef != null) { boolean valid = true; // Ensure that all characters are valid, otherwise encode them for (int i = 0; valid && (i < uriRef.length()); i++) { if (!isValid(uriRef.charAt(i))) { valid = false; Context.getCurrentLogger().fine( "Invalid character detected in URI reference at index '" + i + "': \"" + uriRef.charAt(i) + "\". It will be automatically encoded."); } else if ((uriRef.charAt(i) == '%') && (i > uriRef.length() - 2)) { // A percent encoding character has been detected but // without the necessary two hexadecimal digits following valid = false; Context.getCurrentLogger().fine( "Invalid percent encoding detected in URI reference at index '" + i + "': \"" + uriRef.charAt(i) + "\". It will be automatically encoded."); } } if (!valid) { StringBuilder sb = new StringBuilder(); for (int i = 0; (i < uriRef.length()); i++) { if (isValid(uriRef.charAt(i))) { if ((uriRef.charAt(i) == '%') && (i > uriRef.length() - 2)) { sb.append("%25"); } else { sb.append(uriRef.charAt(i)); } } else { sb.append(encode(String.valueOf(uriRef.charAt(i)))); } } result = sb.toString(); } } return result; } /** * Indicates whether some other object is "equal to" this one. * * @param object * The object to compare to. * @return True if this object is the same as the obj argument. */ @Override public boolean equals(Object object) { if (object instanceof Reference) { final Reference ref = (Reference) object; if (this.internalRef == null) { return ref.internalRef == null; } return this.internalRef.equals(ref.internalRef); } return false; } /** * Returns the authority component for hierarchical identifiers. Includes * the user info, host name and the host port number.<br> * Note that no URI decoding is done by this method. * * @return The authority component for hierarchical identifiers. */ public String getAuthority() { final String part = isRelative() ? getRelativePart() : getSchemeSpecificPart(); if ((part != null) && part.startsWith("//")) { int index = part.indexOf('/', 2); if (index != -1) { return part.substring(2, index); } index = part.indexOf('?'); if (index != -1) { return part.substring(2, index); } return part.substring(2); } return null; } /** * Returns the optionnally decoded authority component. * * @param decode * Indicates if the result should be decoded using the * {@link #decode(String)} method. * @return The optionnally decoded authority component. * @see #getAuthority() */ public String getAuthority(boolean decode) { return decode ? decode(getAuthority()) : getAuthority(); } /** * Returns the base reference for relative references. * * @return The base reference for relative references. */ public Reference getBaseRef() { return this.baseRef; } /** * Returns the optional extensions for hierarchical identifiers. An * extensions part starts after the first '.' character of the last path * segment and ends with either the end of the segment of with the first ';' * character (matrix start). It is a token similar to file extensions * separated by '.' characters. The value can be ommited.<br> * Note that no URI decoding is done by this method. * * @return The extensions or null. * @see #getExtensionsAsArray() * @see #setExtensions(String) */ public String getExtensions() { String result = null; final String lastSegment = getLastSegment(); if (lastSegment != null) { final int extensionIndex = lastSegment.indexOf('.'); final int matrixIndex = lastSegment.indexOf(';'); if (extensionIndex != -1) { // Extensions found if (matrixIndex != -1) { result = lastSegment.substring(extensionIndex + 1, matrixIndex); } else { // No matrix found result = lastSegment.substring(extensionIndex + 1); } } } return result; } /** * Returns the extensions as an array or null if no extension is found. * * @return The extensions as an array or null if no extension is found. * @see #getExtensions() */ public String[] getExtensionsAsArray() { String[] result = null; final String extensions = getExtensions(); if (extensions != null) { result = extensions.split("\\."); } return result; } /** * Returns the fragment identifier.<br> * Note that no URI decoding is done by this method. * * @return The fragment identifier. */ public String getFragment() { if (hasFragment()) { return this.internalRef.substring(this.fragmentIndex + 1); } return null; } /** * Returns the optionnally decoded fragment identifier. * * @param decode * Indicates if the result should be decoded using the * {@link #decode(String)} method. * @return The optionnally decoded fragment identifier. * @see #getFragment() */ public String getFragment(boolean decode) { return decode ? decode(getFragment()) : getFragment(); } /** * Returns the hierarchical part which is equivalent to the scheme specific * part less the query component.<br> * Note that no URI decoding is done by this method. * * @return The hierarchical part . */ public String getHierarchicalPart() { if (hasScheme()) { // Scheme found if (hasQuery()) { // Query found return this.internalRef.substring(this.schemeIndex + 1, this.queryIndex); } // No query found if (hasFragment()) { // Fragment found return this.internalRef.substring(this.schemeIndex + 1, this.fragmentIndex); } // No fragment found return this.internalRef.substring(this.schemeIndex + 1); } // No scheme found if (hasQuery()) { // Query found return this.internalRef.substring(0, this.queryIndex); } if (hasFragment()) { // Fragment found return this.internalRef.substring(0, this.fragmentIndex); } // No fragment found return this.internalRef; } /** * Returns the optionnally decoded hierarchical part. * * @param decode * Indicates if the result should be decoded using the * {@link #decode(String)} method. * @return The optionnally decoded hierarchical part. * @see #getHierarchicalPart() */ public String getHierarchicalPart(boolean decode) { return decode ? decode(getHierarchicalPart()) : getHierarchicalPart(); } /** * Returns the host domain name component for server based hierarchical * identifiers. It can also be replaced by an IP address when no domain name * was registered.<br> * Note that no URI decoding is done by this method. * * @return The host domain name component for server based hierarchical * identifiers. */ public String getHostDomain() { String result = null; final String authority = getAuthority(); if (authority != null) { // We must prevent the case where the userinfo part contains ':' // and the case of IPV6 addresses int indexUI = authority.indexOf('@'); // user info int indexIPV6 = authority.indexOf(']'); // IPV6 int indexP = authority.indexOf(':', (indexIPV6 == -1) ? indexUI : indexIPV6); if (indexUI != -1) { // User info found if (indexP != -1) { // Port found result = authority.substring(indexUI + 1, indexP); } else { // No port found result = authority.substring(indexUI + 1); } } else { // No user info found if (indexP != -1) { // Port found result = authority.substring(0, indexP); } else { // No port found result = authority; } } } return result; } /** * Returns the optionnally decoded host domain name component. * * @param decode * Indicates if the result should be decoded using the * {@link #decode(String)} method. * @return The optionnally decoded host domain name component. * @see #getHostDomain() */ public String getHostDomain(boolean decode) { return decode ? decode(getHostDomain()) : getHostDomain(); } /** * Returns the host identifier. Includes the scheme, the host name and the * host port number.<br> * Note that no URI decoding is done by this method. * * @return The host identifier. */ public String getHostIdentifier() { final StringBuilder result = new StringBuilder(); result.append(getScheme()).append("://").append(getAuthority()); return result.toString(); } /** * Returns the optionnally decoded host identifier. * * @param decode * Indicates if the result should be decoded using the * {@link #decode(String)} method. * @return The optionnally decoded host identifier. * @see #getHostIdentifier() */ public String getHostIdentifier(boolean decode) { return decode ? decode(getHostIdentifier()) : getHostIdentifier(); } /** * Returns the optional port number for server based hierarchical * identifiers. * * @return The optional port number for server based hierarchical * identifiers or -1 if the port number does not exist. */ public int getHostPort() { int result = -1; final String authority = getAuthority(); if (authority != null) { // We must prevent the case where the userinfo part contains ':' // and the case of IPV6 addresses int indexUI = authority.indexOf('@'); // user info int indexIPV6 = authority.indexOf(']'); // IPV6 int index = authority.indexOf(':', (indexIPV6 == -1) ? indexUI : indexIPV6); if (index != -1) { try { result = Integer.parseInt(authority.substring(index + 1)); } catch (NumberFormatException nfe) { Context.getCurrentLogger().log( Level.WARNING, "Can't parse hostPort : [hostRef,requestUri]=[" + getBaseRef() + "," + this.internalRef + "]"); } } } return result; } /** * Returns the absolute resource identifier, without the fragment.<br> * Note that no URI decoding is done by this method. * * @return The absolute resource identifier, without the fragment. */ public String getIdentifier() { if (hasFragment()) { // Fragment found return this.internalRef.substring(0, this.fragmentIndex); } // No fragment found return this.internalRef; } /** * Returns the optionnally decoded absolute resource identifier. * * @param decode * Indicates if the result should be decoded using the * {@link #decode(String)} method. * @return The optionnally decoded absolute resource identifier. * @see #getIdentifier() */ public String getIdentifier(boolean decode) { return decode ? decode(getIdentifier()) : getIdentifier(); } /** * Returns the last segment of a hierarchical path.<br> * For example the "/a/b/c" and "/a/b/c/" paths have the same segments: "a", * "b", "c.<br> * Note that no URI decoding is done by this method. * * @return The last segment of a hierarchical path. */ public String getLastSegment() { String result = null; String path = getPath(); if (path != null) { if (path.endsWith("/")) { path = path.substring(0, path.length() - 1); } final int lastSlash = path.lastIndexOf('/'); if (lastSlash != -1) { result = path.substring(lastSlash + 1); } } return result; } /** * Returns the optionnally decoded last segment. * * @param decode * Indicates if the result should be decoded using the * {@link #decode(String)} method. * @return The optionnally decoded last segment. * @see #getLastSegment() */ public String getLastSegment(boolean decode) { return getLastSegment(decode, false); } /** * Returns the optionnally decoded last segment. * * @param decode * Indicates if the result should be decoded using the * {@link #decode(String)} method. * @param excludeMatrix * @return The optionnally decoded last segment. * @see #getLastSegment() */ public String getLastSegment(boolean decode, boolean excludeMatrix) { String result = getLastSegment(); if (excludeMatrix && (result != null)) { final int matrixIndex = result.indexOf(';'); if (matrixIndex != -1) { result = result.substring(0, matrixIndex); } } return decode ? decode(result) : result; } /** * Returns the optional matrix for hierarchical identifiers. A matrix part * starts after the first ';' character of the last path segment. It is a * sequence of 'name=value' parameters separated by ';' characters. The * value can be ommitted.<br> * Note that no URI decoding is done by this method. * * @return The matrix or null. */ public String getMatrix() { String lastSegment = getLastSegment(); if (lastSegment != null) { final int matrixIndex = lastSegment.indexOf(';'); if (matrixIndex != -1) { return lastSegment.substring(matrixIndex + 1); } } // No matrix found return null; } /** * Returns the optionnally decoded matrix. * * @param decode * Indicates if the result should be decoded using the * {@link #decode(String)} method. * @return The optionnally decoded matrix. * @see #getMatrix() */ public String getMatrix(boolean decode) { return decode ? decode(getMatrix()) : getMatrix(); } /** * Returns the optional matrix as a form. * * @return The optional matrix component as a form. */ public Form getMatrixAsForm() { return new Form(getMatrix(), ';'); } /** * Returns the optional matrix as a form submission. * * @param characterSet * The supported character encoding. * @return The optional matrix as a form. */ public Form getMatrixAsForm(CharacterSet characterSet) { return new Form(getMatrix(), characterSet, ';'); } /** * Returns the parent reference of a hierarchical reference. The last slash * of the path will be considered as the end of the parent path. * * @return The parent reference of a hierarchical reference. */ public Reference getParentRef() { Reference result = null; if (isHierarchical()) { String parentRef = null; String path = getPath(); if (!path.equals("/") && !path.equals("")) { if (path.endsWith("/")) { path = path.substring(0, path.length() - 1); } if (isAbsolute()) { parentRef = getHostIdentifier() + path.substring(0, path.lastIndexOf('/') + 1); } else { parentRef = path.substring(0, path.lastIndexOf('/') + 1); } } else { parentRef = this.internalRef; } result = new Reference(parentRef); } return result; } /** * Returns the path component for hierarchical identifiers. If not path is * available it returns null.<br> * Note that no URI decoding is done by this method. * * @return The path component for hierarchical identifiers. */ public String getPath() { String result = null; String part = isRelative() ? getRelativePart() : getSchemeSpecificPart(); if (part != null) { if (part.startsWith("//")) { // Authority found int index1 = part.indexOf('/', 2); if (index1 != -1) { // Path found int index2 = part.indexOf('?'); if (index2 != -1) { // Query found result = part.substring(Math.min(index1, index2), index2); } else { // No query found result = part.substring(index1); } } else { // Path must be empty in this case } } else { // No authority found int index = part.indexOf('?'); if (index != -1) { // Query found result = part.substring(0, index); } else { // No query found result = part; } } } return result; } /** * Returns the optionnally decoded path component. If not path is available * it returns null. * * @param decode * Indicates if the result should be decoded using the * {@link #decode(String)} method. * @return The optionnally decoded path component. * @see #getPath() */ public String getPath(boolean decode) { return decode ? decode(getPath()) : getPath(); } /** * Returns the optional query component for hierarchical identifiers.<br> * Note that no URI decoding is done by this method. * * @return The query component or null. */ public String getQuery() { if (hasQuery()) { // Query found if (hasFragment()) { if (this.queryIndex < this.fragmentIndex) { // Fragment found and query sign not inside fragment return this.internalRef.substring(this.queryIndex + 1, this.fragmentIndex); } return null; } // No fragment found return this.internalRef.substring(this.queryIndex + 1); } // No query found return null; } /** * Returns the optionnally decoded query component. * * @param decode * Indicates if the result should be decoded using the * {@link #decode(String)} method. * @return The optionnally decoded query component. * @see #getQuery() */ public String getQuery(boolean decode) { return decode ? decode(getQuery()) : getQuery(); } /** * Returns the optional query component as a form. * * @return The optional query component as a form. */ public Form getQueryAsForm() { return new Form(getQuery()); } /** * Returns the optional query component as a form. * * @param decode * Indicates if the names and values should be automatically * decoded. * @return The optional query component as a form. */ public Form getQueryAsForm(boolean decode) { return new Form(getQuery(), decode); } /** * Returns the optional query component as a form submission. * * @param characterSet * The supported character encoding. * @return The optional query component as a form submission. */ public Form getQueryAsForm(CharacterSet characterSet) { return new Form(getQuery(), characterSet); } /** * Returns the relative part of relative references, without the query and * fragment. If the reference is absolute, then null is returned.<br> * Note that no URI decoding is done by this method. * * @return The relative part. */ public String getRelativePart() { return isRelative() ? toString(false, false) : null; } /** * Returns the optionnally decoded relative part. * * @param decode * Indicates if the result should be decoded using the * {@link #decode(String)} method. * @return The optionnally decoded relative part. * @see #getRelativePart() */ public String getRelativePart(boolean decode) { return decode ? decode(getRelativePart()) : getRelativePart(); } /** * Returns the current reference as a relative reference to the current base * reference. This method should only be invoked for absolute references, * otherwise an IllegalArgumentException will be raised. * * @return The current reference as a relative reference to the current base * reference. * @see #getRelativeRef(Reference) */ public Reference getRelativeRef() { return getRelativeRef(getBaseRef()); } /** * Returns the current reference relatively to a base reference. This method * should only be invoked for absolute references, otherwise an * IllegalArgumentException will be raised. * * @param base * The base reference to use. * @throws IllegalArgumentException * If the relative reference is computed although the reference * or the base reference are not absolute or not hierarchical. * @return The current reference relatively to a base reference. */ public Reference getRelativeRef(Reference base) { Reference result = null; if (base == null) { result = this; } else if (!isAbsolute() || !isHierarchical()) { throw new IllegalArgumentException( "The reference must have an absolute hierarchical path component"); } else if (!base.isAbsolute() || !base.isHierarchical()) { throw new IllegalArgumentException( "The base reference must have an absolute hierarchical path component"); } else if (!getHostIdentifier().equals(base.getHostIdentifier())) { result = this; } else { final String localPath = getPath(); final String basePath = base.getPath(); String relativePath = null; if ((basePath == null) || (localPath == null)) { relativePath = localPath; } else { // Find the junction point boolean diffFound = false; int lastSlashIndex = -1; int i = 0; char current; while (!diffFound && (i < localPath.length()) && (i < basePath.length())) { current = localPath.charAt(i); if (current != basePath.charAt(i)) { diffFound = true; } else { if (current == '/') { lastSlashIndex = i; } i++; } } if (!diffFound) { if (localPath.length() == basePath.length()) { // Both paths are strictly equivalent relativePath = "."; } else if (i == localPath.length()) { // End of local path reached if (basePath.charAt(i) == '/') { if ((i + 1) == basePath.length()) { // Both paths are strictly equivalent relativePath = "."; } else { // The local path is a direct parent of the base // path // We need to add enough ".." in the relative // path final StringBuilder sb = new StringBuilder(); // Count segments int segments = 0; for (int j = basePath.indexOf('/', i); j != -1; j = basePath .indexOf('/', j + 1)) segments++; // Build relative path for (int j = 0; j < segments; j++) sb.append("../"); int lastLocalSlash = localPath.lastIndexOf('/'); sb.append(localPath .substring(lastLocalSlash + 1)); relativePath = sb.toString(); } } else { // The base path has a segment that starts like // the last local path segment // But that is longer. Situation similar to a // junction final StringBuilder sb = new StringBuilder(); // Count segments int segments = 0; for (int j = basePath.indexOf('/', i); j != -1; j = basePath .indexOf('/', j + 1)) segments++; // Build relative path for (int j = 0; j < segments; j++) if (j > 0) sb.append("/.."); else sb.append(".."); relativePath = sb.toString(); if (relativePath.equals("")) { relativePath = "."; } } } else if (i == basePath.length()) { if (localPath.charAt(i) == '/') { if ((i + 1) == localPath.length()) { // Both paths are strictly equivalent relativePath = "."; } else { // The local path is a direct child of the base // path relativePath = localPath.substring(i + 1); } } else { if (lastSlashIndex == (i - 1)) { // The local path is a direct subpath of the // base path relativePath = localPath.substring(i); } else { relativePath = ".." + localPath.substring(lastSlashIndex); } } } } else { // We found a junction point, we need to add enough ".." in // the relative path and append the rest of the local path // the local path is a direct subpath of the base path final StringBuilder sb = new StringBuilder(); // Count segments int segments = 0; for (int j = basePath.indexOf('/', i); j != -1; j = basePath .indexOf('/', j + 1)) segments++; // Build relative path for (int j = 0; j < segments; j++) sb.append("../"); sb.append(localPath.substring(lastSlashIndex + 1)); relativePath = sb.toString(); } } // Build the result reference result = new Reference(); final String query = getQuery(); final String fragment = getFragment(); boolean modified = false; if ((query != null) && (!query.equals(base.getQuery()))) { result.setQuery(query); modified = true; } if ((fragment != null) && (!fragment.equals(base.getFragment()))) { result.setFragment(fragment); modified = true; } if (!modified || !relativePath.equals(".")) { result.setPath(relativePath); } } return result; } /** * Returns the part of the resource identifier remaining after the base * reference. Note that the optional fragment is not returned by this * method. Must be used with the following prerequisites: * <ul> * <li>the reference is absolute</li> * <li>the reference identifier starts with the resource baseRef</li> * </ul> * <br> * Note that no URI decoding is done by this method. * * @return The remaining resource part or null if the prerequisites are not * satisfied. * @see #getRemainingPart(boolean) */ public String getRemainingPart() { return getRemainingPart(false, true); } /** * Returns the optionally decoded remaining part. * * @param decode * Indicates if the result should be decoded using the * {@link #decode(String)} method. * @return The optionally decoded remaining part. * @see #getRemainingPart() */ public String getRemainingPart(boolean decode) { return getRemainingPart(decode, true); } /** * Returns the optionally decoded remaining part with or without the query * part of the reference. * * @param decode * Indicates if the result should be decoded using the * {@link #decode(String)} method. * @param query * True if the query part should be returned, false otherwise. * @return The optionally decoded remaining part. * @see #getRemainingPart() */ public String getRemainingPart(boolean decode, boolean query) { String result = null; final String all = toString(query, false); if (getBaseRef() != null) { final String base = getBaseRef().toString(query, false); if ((base != null) && all.startsWith(base)) { result = all.substring(base.length()); } } else { result = all; } return decode ? decode(result) : result; } /** * Returns the scheme component.<br> * Note that no URI decoding is done by this method. * * @return The scheme component. */ public String getScheme() { if (hasScheme()) { // Scheme found return this.internalRef.substring(0, this.schemeIndex); } // No scheme found return null; } /** * Returns the optionnally decoded scheme component. * * @param decode * Indicates if the result should be decoded using the * {@link #decode(String)} method. * @return The optionnally decoded scheme component. * @see #getScheme() */ public String getScheme(boolean decode) { return decode ? decode(getScheme()) : getScheme(); } /** * Returns the protocol associated with the scheme component. * * @return The protocol associated with the scheme component. */ public Protocol getSchemeProtocol() { return Protocol.valueOf(getScheme()); } /** * Returns the scheme specific part.<br> * Note that no URI decoding is done by this method. * * @return The scheme specific part. */ public String getSchemeSpecificPart() { String result = null; if (hasScheme()) { // Scheme found if (hasFragment()) { // Fragment found result = this.internalRef.substring(this.schemeIndex + 1, this.fragmentIndex); } else { // No fragment found result = this.internalRef.substring(this.schemeIndex + 1); } } return result; } /** * Returns the optionnally decoded scheme specific part. * * @param decode * Indicates if the result should be decoded using the * {@link #decode(String)} method. * @return The optionnally decoded scheme specific part. * @see #getSchemeSpecificPart() */ public String getSchemeSpecificPart(boolean decode) { return decode ? decode(getSchemeSpecificPart()) : getSchemeSpecificPart(); } /** * Returns the list of segments in a hierarchical path.<br> * A new list is created for each call.<br> * Note that no URI decoding is done by this method. * * @return The segments of a hierarchical path. */ public List<String> getSegments() { final List<String> result = new ArrayList<String>(); final String path = getPath(); int start = -2; // The index of the slash starting the segment char current; if (path != null) { for (int i = 0; i < path.length(); i++) { current = path.charAt(i); if (current == '/') { if (start == -2) { // Beginning of an absolute path or sequence of two // separators start = i; } else { // End of a segment result.add(path.substring(start + 1, i)); start = i; } } else { if (start == -2) { // Starting a new segment for a relative path start = -1; } else { // Looking for the next character } } } if (start != -2) { // Add the last segment result.add(path.substring(start + 1)); } } return result; } /** * Returns the optionnally decoded list of segments. * * @param decode * Indicates if the result should be decoded using the * {@link #decode(String)} method. * @return The optionnally decoded list of segments. * @see #getSegments() */ public List<String> getSegments(boolean decode) { final List<String> result = getSegments(); if (decode) { for (int i = 0; i < result.size(); i++) { result.set(i, decode(result.get(i))); } } return result; } /** * Returns the target reference. This method resolves relative references * against the base reference then normalize them. * * @throws IllegalArgumentException * If the base reference (after resolution) is not absolute. * @throws IllegalArgumentException * If the reference is relative and not base reference has been * provided. * * @return The target reference. */ public Reference getTargetRef() { Reference result = null; // Step 1 - Resolve relative reference against their base reference if (isRelative() && (this.baseRef != null)) { Reference baseReference = null; if (this.baseRef.isAbsolute()) { baseReference = this.baseRef; } else { baseReference = this.baseRef.getTargetRef(); } if (baseReference.isRelative()) { throw new IllegalArgumentException( "The base reference must have an absolute hierarchical path component"); } // Relative URI detected String authority = getAuthority(); String path = getPath(); String query = getQuery(); String fragment = getFragment(); // Create an empty reference result = new Reference(); result.setScheme(baseReference.getScheme()); if (authority != null) { result.setAuthority(authority); result.setPath(path); result.setQuery(query); } else { result.setAuthority(baseReference.getAuthority()); if ((path == null) || (path.equals(""))) { result.setPath(baseReference.getPath()); if (query != null) { result.setQuery(query); } else { result.setQuery(baseReference.getQuery()); } } else { if (path.startsWith("/")) { result.setPath(path); } else { final String basePath = baseReference.getPath(); String mergedPath = null; if ((baseReference.getAuthority() != null) && ((basePath == null) || (basePath.equals("")))) { mergedPath = "/" + path; } else { // Remove the last segment which may be empty if // the path is ending with a slash final int lastSlash = basePath.lastIndexOf('/'); if (lastSlash == -1) { mergedPath = path; } else { mergedPath = basePath.substring(0, lastSlash + 1) + path; } } result.setPath(mergedPath); } result.setQuery(query); } } result.setFragment(fragment); } else if (isRelative()) { // Relative reference with no baseRef detected throw new IllegalArgumentException( "Relative references are only usable when a base reference is set."); } else { // Absolute URI detected result = new Reference(this.internalRef); } // Step 2 - Normalize the target reference result.normalize(); return result; } /** * Returns the user info component for server based hierarchical * identifiers.<br> * Note that no URI decoding is done by this method. * * @return The user info component for server based hierarchical * identifiers. */ public String getUserInfo() { String result = null; final String authority = getAuthority(); if (authority != null) { final int index = authority.indexOf('@'); if (index != -1) { result = authority.substring(0, index); } } return result; } /** * Returns the optionnally decoded user info component. * * @param decode * Indicates if the result should be decoded using the * {@link #decode(String)} method. * @return The optionnally decoded user info component. * @see #getUserInfo() */ public String getUserInfo(boolean decode) { return decode ? decode(getUserInfo()) : getUserInfo(); } /** * Indicates if this reference has file-like extensions on its last path * segment. * * @return True if there is are extensions. * @see #getExtensions() */ public boolean hasExtensions() { boolean result = false; // If these reference ends with a "/", it cannot be a file. final String path = getPath(); if (!((path != null) && path.endsWith("/"))) { final String lastSegment = getLastSegment(); if (lastSegment != null) { final int extensionsIndex = lastSegment.indexOf('.'); final int matrixIndex = lastSegment.indexOf(';'); result = (extensionsIndex != -1) && ((matrixIndex == -1) || (extensionsIndex < matrixIndex)); } } return result; } /** * Indicates if this reference has a fragment identifier. * * @return True if there is a fragment identifier. */ public boolean hasFragment() { return (this.fragmentIndex != -1); } /** * Returns a hash code value for the object. * * @return A hash code value for the object. */ @Override public int hashCode() { return (this.internalRef == null) ? 0 : this.internalRef.hashCode(); } /** * Indicates if this reference has a matrix. * * @return True if there is a matrix. * @see #getMatrix() */ public boolean hasMatrix() { return (getLastSegment().indexOf(';') != -1); } /** * Indicates if this reference has a query component. * * @return True if there is a query. */ public boolean hasQuery() { return (this.queryIndex != -1); } /** * Indicates if this reference has a scheme component. * * @return True if there is a scheme component. */ public boolean hasScheme() { return (this.schemeIndex != -1); } /** * Indicates if the reference is absolute. * * @return True if the reference is absolute. */ public boolean isAbsolute() { return (getScheme() != null); } /** * Returns true if both reference are equivalent, meaning that they resolve * to the same target reference. * * @param ref * The reference to compare. * @return True if both reference are equivalent. */ public boolean isEquivalentTo(Reference ref) { return getTargetRef().equals(ref.getTargetRef()); } /** * Indicates if the identifier is hierarchical. * * @return True if the identifier is hierarchical, false if it is opaque. */ public boolean isHierarchical() { return isRelative() || (getSchemeSpecificPart().charAt(0) == '/'); } /** * Indicates if the identifier is opaque. * * @return True if the identifier is opaque, false if it is hierarchical. */ public boolean isOpaque() { return isAbsolute() && (getSchemeSpecificPart().charAt(0) != '/'); } /** * Indicates if the reference is a parent of the hierarchical child * reference. * * @param childRef * The hierarchical reference. * @return True if the reference is a parent of the hierarchical child * reference. */ public boolean isParent(Reference childRef) { boolean result = false; if ((childRef != null) && (childRef.isHierarchical())) { result = childRef.toString(false, false).startsWith( toString(false, false)); } return result; } /** * Indicates if the reference is relative. * * @return True if the reference is relative. */ public boolean isRelative() { return (getScheme() == null); } /** * Normalizes the reference. Useful before comparison between references or * when building a target reference from a base and a relative references. * * @return The current reference. */ public Reference normalize() { // 1. The input buffer is initialized with the now-appended path // components and the output buffer is initialized to the empty string. StringBuilder output = new StringBuilder(); StringBuilder input = new StringBuilder(); String path = getPath(); if (path != null) { input.append(path); } // 2. While the input buffer is not empty, loop as follows: while (input.length() > 0) { // A. If the input buffer begins with a prefix of "../" or "./", // then remove that prefix from the input buffer; otherwise, if ((input.length() >= 3) && input.substring(0, 3).equals("../")) { input.delete(0, 3); } else if ((input.length() >= 2) && input.substring(0, 2).equals("./")) { input.delete(0, 2); } // B. if the input buffer begins with a prefix of "/./" or "/.", // where "." is a complete path segment, then replace that // prefix with "/" in the input buffer; otherwise, else if ((input.length() >= 3) && input.substring(0, 3).equals("/./")) { input.delete(0, 2); } else if ((input.length() == 2) && input.substring(0, 2).equals("/.")) { input.delete(1, 2); } // C. if the input buffer begins with a prefix of "/../" or "/..", // where ".." is a complete path segment, then replace that prefix // with "/" in the input buffer and remove the last segment and its // preceding "/" (if any) from the output buffer; otherwise, else if ((input.length() >= 4) && input.substring(0, 4).equals("/../")) { input.delete(0, 3); removeLastSegment(output); } else if ((input.length() == 3) && input.substring(0, 3).equals("/..")) { input.delete(1, 3); removeLastSegment(output); } // D. if the input buffer consists only of "." or "..", then remove // that from the input buffer; otherwise, else if ((input.length() == 1) && input.substring(0, 1).equals(".")) { input.delete(0, 1); } else if ((input.length() == 2) && input.substring(0, 2).equals("..")) { input.delete(0, 2); } // E. move the first path segment in the input buffer to the end of // the output buffer, including the initial "/" character (if any) // and any subsequent characters up to, but not including, the next // "/" character or the end of the input buffer. else { int max = -1; for (int i = 1; (max == -1) && (i < input.length()); i++) { if (input.charAt(i) == '/') { max = i; } } if (max != -1) { // We found the next "/" character. output.append(input.substring(0, max)); input.delete(0, max); } else { // End of input buffer reached // [ifndef gwt] instruction output.append(input); // [ifdef gwt] instruction uncomment // output.append(input.toString()); input.delete(0, input.length()); } } } // Finally, the output buffer is returned as the result setPath(output.toString()); // Ensure that the scheme and host names are reset in lower case setScheme(getScheme()); setHostDomain(getHostDomain()); // Remove the port if it is equal to the default port of the reference's // Protocol. final int hostPort = getHostPort(); if (hostPort != -1) { final int defaultPort = Protocol.valueOf(getScheme()) .getDefaultPort(); if (hostPort == defaultPort) { setHostPort(null); } } return this; } /** * Removes the last segement from the output builder. * * @param output * The output builder to update. */ private void removeLastSegment(StringBuilder output) { int min = -1; for (int i = output.length() - 1; (min == -1) && (i >= 0); i--) { if (output.charAt(i) == '/') { min = i; } } if (min != -1) { // We found the previous "/" character. output.delete(min, output.length()); } else { // End of output buffer reached output.delete(0, output.length()); } } /** * Sets the authority component for hierarchical identifiers. * * @param authority * The authority component for hierarchical identifiers. */ public void setAuthority(String authority) { final String oldPart = isRelative() ? getRelativePart() : getSchemeSpecificPart(); String newPart; final String newAuthority = (authority == null) ? "" : "//" + authority; if (oldPart == null) { newPart = newAuthority; } else if (oldPart.startsWith("//")) { int index = oldPart.indexOf('/', 2); if (index != -1) { newPart = newAuthority + oldPart.substring(index); } else { index = oldPart.indexOf('?'); if (index != -1) { newPart = newAuthority + oldPart.substring(index); } else { newPart = newAuthority; } } } else { newPart = newAuthority + oldPart; } if (isAbsolute()) { setSchemeSpecificPart(newPart); } else { setRelativePart(newPart); } } /** * Sets the base reference for relative references. * * @param baseRef * The base reference for relative references. */ public void setBaseRef(Reference baseRef) { this.baseRef = baseRef; } /** * Sets the base reference for relative references. * * @param baseUri * The base URI for relative references. */ public void setBaseRef(String baseUri) { setBaseRef(new Reference(baseUri)); } /** * Sets the extensions for hierarchical identifiers. An extensions part * starts after the first '.' character of the last path segment and ends * with either the end of the segment of with the first ';' character * (matrix start). It is a token similar to file extensions separated by '.' * characters. The value can be ommited.<br> * Note that no URI decoding is done by this method. * * @param extensions * The extensions to set or null (without leading or trailing * dots). * @see #getExtensions() * @see #getExtensionsAsArray() * @see #setExtensions(String[]) */ public void setExtensions(String extensions) { final String lastSegment = getLastSegment(); if (lastSegment != null) { final int extensionIndex = lastSegment.indexOf('.'); final int matrixIndex = lastSegment.indexOf(';'); final StringBuilder sb = new StringBuilder(); if (extensionIndex != -1) { // Extensions found sb.append(lastSegment.substring(0, extensionIndex)); if ((extensions != null) && (extensions.length() > 0)) { sb.append('.').append(extensions); } if (matrixIndex != -1) { sb.append(lastSegment.substring(matrixIndex)); } } else { // Extensions not found if ((extensions != null) && (extensions.length() > 0)) { if (matrixIndex != -1) { // Matrix found, make sure we append it // after the extensions sb.append(lastSegment.substring(0, matrixIndex)) .append('.').append(extensions) .append(lastSegment.substring(matrixIndex)); } else { // No matrix found, just append the extensions sb.append(lastSegment).append('.').append(extensions); } } else { // No change necessary sb.append(lastSegment); } } // Finally update the last segment setLastSegment(sb.toString()); } else { setLastSegment('.' + extensions); } } /** * Sets the extensions based on an array of extension tokens (without dots). * * @param extensions * The array of extensions. * @see #getExtensions() * @see #getExtensionsAsArray() * @see #setExtensions(String) */ public void setExtensions(String[] extensions) { String exts = null; if (extensions != null) { final StringBuilder sb = new StringBuilder(); for (int i = 0; i < extensions.length; i++) { if (i > 0) { sb.append('.'); } sb.append(extensions[i]); } exts = sb.toString(); } setExtensions(exts); } /** * Sets the fragment identifier. * * @param fragment * The fragment identifier. * @throws IllegalArgumentException * if the fragment parameter contains the fragment delimiter * ('#'). */ public void setFragment(String fragment) { fragment = encodeInvalidCharacters(fragment); if ((fragment != null) && (fragment.indexOf('#') != -1)) { throw new IllegalArgumentException( "Illegal '#' character detected in parameter"); } if (hasFragment()) { // Existing fragment if (fragment != null) { this.internalRef = this.internalRef.substring(0, this.fragmentIndex + 1) + fragment; } else { this.internalRef = this.internalRef.substring(0, this.fragmentIndex); } } else { // No existing fragment if (fragment != null) { if (this.internalRef != null) { this.internalRef = this.internalRef + '#' + fragment; } else { this.internalRef = '#' + fragment; } } else { // Do nothing } } updateIndexes(); } /** * Sets the host domain component for server based hierarchical identifiers. * * @param domain * The host component for server based hierarchical identifiers. */ public void setHostDomain(String domain) { final String authority = getAuthority(); if (authority == null) { setAuthority(domain); } else { if (domain == null) { domain = ""; } else { // URI specification indicates that host names should be // produced in lower case domain = domain.toLowerCase(); } // We must prevent the case where the userinfo part contains ':' // and the case of IPV6 addresses int indexUI = authority.indexOf('@'); // user info int indexIPV6 = authority.indexOf(']'); // IPV6 int indexP = authority.indexOf(':', (indexIPV6 == -1) ? indexUI : indexIPV6); if (indexUI != -1) { // User info found if (indexP != -1) { // Port found setAuthority(authority.substring(0, indexUI + 1) + domain + authority.substring(indexP)); } else { // No port found setAuthority(authority.substring(0, indexUI + 1) + domain); } } else { // No user info found if (indexP != -1) { // Port found setAuthority(domain + authority.substring(indexP)); } else { // No port found setAuthority(domain); } } } } /** * Sets the optional port number for server based hierarchical identifiers. * * @param port * The optional port number for server based hierarchical * identifiers. * @throws IllegalArgumentException * If the autority has not been defined. */ public void setHostPort(Integer port) { final String authority = getAuthority(); if (authority != null) { // We must prevent the case where the userinfo part contains ':' // and the case of IPV6 addresses int indexUI = authority.indexOf('@'); // user info int indexIPV6 = authority.indexOf(']'); // IPV6 int index = authority.indexOf(':', (indexIPV6 == -1) ? indexUI : indexIPV6); String newPort = (port == null) ? "" : ":" + port; if (index != -1) { setAuthority(authority.substring(0, index) + newPort); } else { setAuthority(authority + newPort); } } else { throw new IllegalArgumentException( "No authority defined, please define a host name first"); } } /** * Sets the absolute resource identifier. * * @param identifier * The absolute resource identifier. * @throws IllegalArgumentException * If the identifier parameter contains the fragment delimiter * ('#'). */ public void setIdentifier(String identifier) { identifier = encodeInvalidCharacters(identifier); if (identifier == null) { identifier = ""; } if (identifier.indexOf('#') != -1) { throw new IllegalArgumentException( "Illegal '#' character detected in parameter"); } if (hasFragment()) { // Fragment found this.internalRef = identifier + this.internalRef.substring(this.fragmentIndex); } else { // No fragment found this.internalRef = identifier; } updateIndexes(); } /** * Sets the last segment of the path. If no path is available, then it * creates one and adds a slash in front of the given last segmetn. <br> * Note that no URI decoding is done by this method. * * @param lastSegment * The last segment of a hierarchical path. */ public void setLastSegment(String lastSegment) { String path = getPath(); int lastSlashIndex = -1; if (path != null) { lastSlashIndex = path.lastIndexOf('/'); } if (lastSlashIndex != -1) { setPath(path.substring(0, lastSlashIndex + 1) + lastSegment); } else { setPath('/' + lastSegment); } } /** * Sets the path component for hierarchical identifiers. * * @param path * The path component for hierarchical identifiers. */ public void setPath(String path) { final String oldPart = isRelative() ? getRelativePart() : getSchemeSpecificPart(); String newPart = null; if (oldPart != null) { if (path == null) { path = ""; } if (oldPart.startsWith("//")) { // Authority found final int index1 = oldPart.indexOf('/', 2); if (index1 != -1) { // Path found final int index2 = oldPart.indexOf('?'); if (index2 != -1) { // Query found newPart = oldPart.substring(0, index1) + path + oldPart.substring(index2); } else { // No query found newPart = oldPart.substring(0, index1) + path; } } else { // No path found final int index2 = oldPart.indexOf('?'); if (index2 != -1) { // Query found newPart = oldPart.substring(0, index2) + path + oldPart.substring(index2); } else { // No query found newPart = oldPart + path; } } } else { // No authority found final int index = oldPart.indexOf('?'); if (index != -1) { // Query found newPart = path + oldPart.substring(index); } else { // No query found newPart = path; } } } else { newPart = path; } if (isAbsolute()) { setSchemeSpecificPart(newPart); } else { setRelativePart(newPart); } } /** * Sets the scheme component based on this protocol. * * @param protocol * The protocol of the scheme component. */ public void setProtocol(Protocol protocol) { setScheme(protocol.getSchemeName()); } /** * Sets the query component for hierarchical identifiers. * * @param query * The query component for hierarchical identifiers. */ public void setQuery(String query) { query = encodeInvalidCharacters(query); final boolean emptyQueryString = ((query == null) || (query.length() <= 0)); if (hasQuery()) { // Query found if (hasFragment()) { // Fragment found if (!emptyQueryString) { this.internalRef = this.internalRef.substring(0, this.queryIndex + 1) + query + this.internalRef.substring(this.fragmentIndex); } else { this.internalRef = this.internalRef.substring(0, this.queryIndex) + this.internalRef.substring(this.fragmentIndex); } } else { // No fragment found if (!emptyQueryString) { this.internalRef = this.internalRef.substring(0, this.queryIndex + 1) + query; } else { this.internalRef = this.internalRef.substring(0, this.queryIndex); } } } else { // No query found if (hasFragment()) { // Fragment found if (!emptyQueryString) { this.internalRef = this.internalRef.substring(0, this.fragmentIndex) + '?' + query + this.internalRef.substring(this.fragmentIndex); } else { // Do nothing; } } else { // No fragment found if (!emptyQueryString) { if (this.internalRef != null) { this.internalRef = this.internalRef + '?' + query; } else { this.internalRef = '?' + query; } } else { // Do nothing; } } } updateIndexes(); } /** * Sets the relative part for relative references only. * * @param relativePart * The relative part to set. */ public void setRelativePart(String relativePart) { relativePart = encodeInvalidCharacters(relativePart); if (relativePart == null) { relativePart = ""; } if (!hasScheme()) { // This is a relative reference, no scheme found if (hasQuery()) { // Query found this.internalRef = relativePart + this.internalRef.substring(this.queryIndex); } else if (hasFragment()) { // Fragment found this.internalRef = relativePart + this.internalRef.substring(this.fragmentIndex); } else { // No fragment found this.internalRef = relativePart; } } updateIndexes(); } /** * Sets the scheme component. * * @param scheme * The scheme component. */ public void setScheme(String scheme) { scheme = encodeInvalidCharacters(scheme); if (scheme != null) { // URI specification indicates that scheme names should be // produced in lower case scheme = scheme.toLowerCase(); } if (hasScheme()) { // Scheme found if (scheme != null) { this.internalRef = scheme + this.internalRef.substring(this.schemeIndex); } else { this.internalRef = this.internalRef .substring(this.schemeIndex + 1); } } else { // No scheme found if (scheme != null) { if (this.internalRef == null) { this.internalRef = scheme + ':'; } else { this.internalRef = scheme + ':' + this.internalRef; } } } updateIndexes(); } /** * Sets the scheme specific part. * * @param schemeSpecificPart * The scheme specific part. */ public void setSchemeSpecificPart(String schemeSpecificPart) { schemeSpecificPart = encodeInvalidCharacters(schemeSpecificPart); if (schemeSpecificPart == null) { schemeSpecificPart = ""; } if (hasScheme()) { // Scheme found if (hasFragment()) { // Fragment found this.internalRef = this.internalRef.substring(0, this.schemeIndex + 1) + schemeSpecificPart + this.internalRef.substring(this.fragmentIndex); } else { // No fragment found this.internalRef = this.internalRef.substring(0, this.schemeIndex + 1) + schemeSpecificPart; } } else { // No scheme found if (hasFragment()) { // Fragment found this.internalRef = schemeSpecificPart + this.internalRef.substring(this.fragmentIndex); } else { // No fragment found this.internalRef = schemeSpecificPart; } } updateIndexes(); } /** * Sets the segments of a hierarchical path.<br> * A new absolute path will replace any existing one. * * @param segments * The segments of the hierarchical path. */ public void setSegments(List<String> segments) { final StringBuilder sb = new StringBuilder(); for (final String segment : segments) { sb.append('/').append(segment); } setPath(sb.toString()); } /** * Sets the user info component for server based hierarchical identifiers. * * @param userInfo * The user info component for server based hierarchical * identifiers. * @throws IllegalArgumentException * If the autority part has not been defined. */ public void setUserInfo(String userInfo) { final String authority = getAuthority(); if (authority != null) { final int index = authority.indexOf('@'); final String newUserInfo = (userInfo == null) ? "" : userInfo + '@'; if (index != -1) { setAuthority(newUserInfo + authority.substring(index + 1)); } else { setAuthority(newUserInfo + authority); } } else { throw new IllegalArgumentException( "No authority defined, please define a host name first"); } } /** * Returns the reference as an URI string. * * @return The reference as an URI string. */ @Override public String toString() { return this.internalRef; } /** * Returns the URI reference string. * * @param query * Indicates if the query should be included; * @param fragment * Indicates if the fragment should be included; * @return The URI reference string. */ public String toString(boolean query, boolean fragment) { if (query) { if (fragment) { return this.internalRef; } if (hasFragment()) { return this.internalRef.substring(0, this.fragmentIndex); } return this.internalRef; } if (fragment) { // Fragment should be included if (hasQuery()) { // Query found if (hasFragment()) { // Fragment found return this.internalRef.substring(0, this.queryIndex) + "#" + getFragment(); } // No fragment found return this.internalRef.substring(0, this.queryIndex); } // No query found return this.internalRef; } // Fragment should not be included if (hasQuery()) { // Query found return this.internalRef.substring(0, this.queryIndex); } if (hasFragment()) { // Fragment found return this.internalRef.substring(0, this.fragmentIndex); } return this.internalRef; } // [ifndef gwt] method /** * Converts to a {@link java.net.URI} instance. Note that relative * references are resolved before conversion using the * {@link #getTargetRef()} method. * * @return A {@link java.net.URI} instance. */ public java.net.URI toUri() { return java.net.URI.create(getTargetRef().toString()); } // [ifndef gwt] method /** * Converts to a {@link java.net.URL} instance. Note that relative * references are resolved before conversion using the * {@link #getTargetRef()} method. * * @return A {@link java.net.URL} instance. */ public java.net.URL toUrl() { java.net.URL result = null; try { result = new java.net.URL(getTargetRef().toString()); } catch (java.net.MalformedURLException e) { throw new IllegalArgumentException("Malformed URL exception", e); } return result; } /** * Updates internal indexes. */ private void updateIndexes() { if (this.internalRef != null) { // Compute the indexes final int firstSlashIndex = this.internalRef.indexOf('/'); this.schemeIndex = this.internalRef.indexOf(':'); if ((firstSlashIndex != -1) && (this.schemeIndex > firstSlashIndex)) { // We are in the rare case of a relative reference where one of // the path segments contains a colon character. In this case, // we ignore the colon as a valid scheme index. // Note that this colon can't be in the first segment as it is // forbidden by the URI RFC. this.schemeIndex = -1; } this.queryIndex = this.internalRef.indexOf('?'); this.fragmentIndex = this.internalRef.indexOf('#'); if (hasQuery() && hasFragment() && (this.queryIndex > this.fragmentIndex)) { // Query sign inside fragment this.queryIndex = -1; } if (hasQuery() && this.schemeIndex > this.queryIndex) { // Colon sign inside query this.schemeIndex = -1; } if (hasFragment() && this.schemeIndex > this.fragmentIndex) { // Colon sign inside fragment this.schemeIndex = -1; } } else { this.schemeIndex = -1; this.queryIndex = -1; this.fragmentIndex = -1; } } }