/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package java.net;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Locale;
import libcore.net.UriCodec;
import libcore.net.url.UrlUtils;
/**
* A Uniform Resource Identifier that identifies an abstract or physical
* resource, as specified by <a href="http://www.ietf.org/rfc/rfc2396.txt">RFC
* 2396</a>.
*
* <h3>Parts of a URI</h3>
* A URI is composed of many parts. This class can both parse URI strings into
* parts and compose URI strings from parts. For example, consider the parts of
* this URI:
* {@code http://username:password@host:8080/directory/file?query#fragment}
* <table>
* <tr><th>Component </th><th>Example value </th><th>Also known as</th></tr>
* <tr><td>{@link #getScheme() Scheme} </td><td>{@code http} </td><td>protocol</td></tr>
* <tr><td>{@link #getSchemeSpecificPart() Scheme-specific part}</td><td>{@code //username:password@host:8080/directory/file?query#fragment}</td><td></td></tr>
* <tr><td>{@link #getAuthority() Authority} </td><td>{@code username:password@host:8080} </td><td></td></tr>
* <tr><td>{@link #getUserInfo() User Info} </td><td>{@code username:password} </td><td></td></tr>
* <tr><td>{@link #getHost() Host} </td><td>{@code host} </td><td></td></tr>
* <tr><td>{@link #getPort() Port} </td><td>{@code 8080} </td><td></td></tr>
* <tr><td>{@link #getPath() Path} </td><td>{@code /directory/file} </td><td></td></tr>
* <tr><td>{@link #getQuery() Query} </td><td>{@code query} </td><td></td></tr>
* <tr><td>{@link #getFragment() Fragment} </td><td>{@code fragment} </td><td>ref</td></tr>
* </table>
*
* <h3>Absolute vs. Relative URIs</h3>
* URIs are either {@link #isAbsolute() absolute or relative}.
* <ul>
* <li><strong>Absolute:</strong> {@code http://android.com/robots.txt}
* <li><strong>Relative:</strong> {@code robots.txt}
* </ul>
*
* <p>Absolute URIs always have a scheme. If its scheme is supported by {@link
* URL}, you can use {@link #toURL} to convert an absolute URI to a URL.
*
* <p>Relative URIs do not have a scheme and cannot be converted to URLs. If you
* have the absolute URI that a relative URI is relative to, you can use {@link
* #resolve} to compute the referenced absolute URI. Symmetrically, you can use
* {@link #relativize} to compute the relative URI from one URI to another.
* <pre> {@code
* URI absolute = new URI("http://android.com/");
* URI relative = new URI("robots.txt");
* URI resolved = new URI("http://android.com/robots.txt");
*
* // print "http://android.com/robots.txt"
* System.out.println(absolute.resolve(relative));
*
* // print "robots.txt"
* System.out.println(absolute.relativize(resolved));
* }</pre>
*
* <h3>Opaque vs. Hierarchical URIs</h3>
* Absolute URIs are either {@link #isOpaque() opaque or hierarchical}. Relative
* URIs are always hierarchical.
* <ul>
* <li><strong>Hierarchical:</strong> {@code http://android.com/robots.txt}
* <li><strong>Opaque:</strong> {@code mailto:robots@example.com}
* </ul>
*
* <p>Opaque URIs have both a scheme and a scheme-specific part that does not
* begin with the slash character: {@code /}. The contents of the
* scheme-specific part of an opaque URI is not parsed so an opaque URI never
* has an authority, user info, host, port, path or query. An opaque URIs may
* have a fragment, however. A typical opaque URI is
* {@code mailto:robots@example.com}.
* <table>
* <tr><th>Component </th><th>Example value </th></tr>
* <tr><td>Scheme </td><td>{@code mailto} </td></tr>
* <tr><td>Scheme-specific part</td><td>{@code robots@example.com}</td></tr>
* <tr><td>Fragment </td><td> </td></tr>
* </table>
* <p>Hierarchical URIs may have values for any URL component. They always
* have a non-null path, though that path may be the empty string.
*
* <h3>Encoding and Decoding URI Components</h3>
* Each component of a URI permits a limited set of legal characters. Other
* characters must first be <i>encoded</i> before they can be embedded in a URI.
* To recover the original characters from a URI, they may be <i>decoded</i>.
* <strong>Contrary to what you might expect,</strong> this class uses the
* term <i>raw</i> to refer to encoded strings. The non-<i>raw</i> accessors
* return decoded strings. For example, consider how this URI is decoded:
* {@code http://user:pa55w%3Frd@host:80/doc%7Csearch?q=green%20robots#over%206%22}
* <table>
* <tr><th>Component </th><th>Legal Characters </th><th>Other Constraints </th><th>Raw Value </th><th>Value</th></tr>
* <tr><td>Scheme </td><td>{@code 0-9}, {@code a-z}, {@code A-Z}, {@code +-.} </td><td>First character must be in {@code a-z}, {@code A-Z}</td><td> </td><td>{@code http}</td></tr>
* <tr><td>Scheme-specific part</td><td>{@code 0-9}, {@code a-z}, {@code A-Z}, {@code _-!.~'()*,;:$&+=?/[]@}</td><td>Non-ASCII characters okay </td><td>{@code //user:pa55w%3Frd@host:80/doc%7Csearch?q=green%20robots}</td><td>{@code //user:pa55w?rd@host:80/doc|search?q=green robots}</td></tr>
* <tr><td>Authority </td><td>{@code 0-9}, {@code a-z}, {@code A-Z}, {@code _-!.~'()*,;:$&+=@[]} </td><td>Non-ASCII characters okay </td><td>{@code user:pa55w%3Frd@host:80} </td><td>{@code user:pa55w?rd@host:80}</td></tr>
* <tr><td>User Info </td><td>{@code 0-9}, {@code a-z}, {@code A-Z}, {@code _-!.~'()*,;:$&+=} </td><td>Non-ASCII characters okay </td><td>{@code user:pa55w%3Frd} </td><td>{@code user:pa55w?rd}</td></tr>
* <tr><td>Host </td><td>{@code 0-9}, {@code a-z}, {@code A-Z}, {@code -.[]} </td><td>Domain name, IPv4 address or [IPv6 address] </td><td> </td><td>host</td></tr>
* <tr><td>Port </td><td>{@code 0-9} </td><td> </td><td> </td><td>{@code 80}</td></tr>
* <tr><td>Path </td><td>{@code 0-9}, {@code a-z}, {@code A-Z}, {@code _-!.~'()*,;:$&+=/@} </td><td>Non-ASCII characters okay </td><td>{@code /doc%7Csearch} </td><td>{@code /doc|search}</td></tr>
* <tr><td>Query </td><td>{@code 0-9}, {@code a-z}, {@code A-Z}, {@code _-!.~'()*,;:$&+=?/[]@}</td><td>Non-ASCII characters okay </td><td>{@code q=green%20robots} </td><td>{@code q=green robots}</td></tr>
* <tr><td>Fragment </td><td>{@code 0-9}, {@code a-z}, {@code A-Z}, {@code _-!.~'()*,;:$&+=?/[]@}</td><td>Non-ASCII characters okay </td><td>{@code over%206%22} </td><td>{@code over 6"}</td></tr>
* </table>
* A URI's host, port and scheme are not eligible for encoding and must not
* contain illegal characters.
*
* <p>To encode a URI, invoke any of the multiple-parameter constructors of this
* class. These constructors accept your original strings and encode them into
* their raw form.
*
* <p>To decode a URI, invoke the single-string constructor, and then use the
* appropriate accessor methods to get the decoded components.
*
* <p>The {@link URL} class can be used to retrieve resources by their URI.
*/
public final class URI implements Comparable<URI>, Serializable {
private static final long serialVersionUID = -6052424284110960213l;
static final String UNRESERVED = "_-!.~\'()*";
static final String PUNCTUATION = ",;:$&+=";
static final UriCodec USER_INFO_ENCODER = new PartEncoder("");
static final UriCodec PATH_ENCODER = new PartEncoder("/@");
static final UriCodec AUTHORITY_ENCODER = new PartEncoder("@[]");
/** for java.net.URL, which foolishly combines these two parts */
static final UriCodec FILE_AND_QUERY_ENCODER = new PartEncoder("/@?");
/** for query, fragment, and scheme-specific part */
static final UriCodec ALL_LEGAL_ENCODER = new PartEncoder("?/[]@");
/** Retains all ASCII chars including delimiters. */
private static final UriCodec ASCII_ONLY = new UriCodec() {
@Override protected boolean isRetained(char c) {
return c <= 127;
}
};
/**
* Encodes the unescaped characters of {@code s} that are not permitted.
* Permitted characters are:
* <ul>
* <li>Unreserved characters in <a href="http://www.ietf.org/rfc/rfc2396.txt">RFC 2396</a>.
* <li>{@code extraOkayChars},
* <li>non-ASCII, non-control, non-whitespace characters
* </ul>
*/
private static class PartEncoder extends UriCodec {
private final String extraLegalCharacters;
PartEncoder(String extraLegalCharacters) {
this.extraLegalCharacters = extraLegalCharacters;
}
@Override protected boolean isRetained(char c) {
return UNRESERVED.indexOf(c) != -1
|| PUNCTUATION.indexOf(c) != -1
|| extraLegalCharacters.indexOf(c) != -1
|| (c > 127 && !Character.isSpaceChar(c) && !Character.isISOControl(c));
}
}
private String string;
private transient String scheme;
private transient String schemeSpecificPart;
private transient String authority;
private transient String userInfo;
private transient String host;
private transient int port = -1;
private transient String path;
private transient String query;
private transient String fragment;
private transient boolean opaque;
private transient boolean absolute;
private transient boolean serverAuthority = false;
private transient int hash = -1;
private URI() {}
/**
* Creates a new URI instance by parsing {@code spec}.
*
* @param spec a URI whose illegal characters have all been encoded.
*/
public URI(String spec) throws URISyntaxException {
parseURI(spec, false);
}
/**
* Creates a new URI instance of the given unencoded component parts.
*
* @param scheme the URI scheme, or null for a non-absolute URI.
*/
public URI(String scheme, String schemeSpecificPart, String fragment)
throws URISyntaxException {
StringBuilder uri = new StringBuilder();
if (scheme != null) {
uri.append(scheme);
uri.append(':');
}
if (schemeSpecificPart != null) {
ALL_LEGAL_ENCODER.appendEncoded(uri, schemeSpecificPart);
}
if (fragment != null) {
uri.append('#');
ALL_LEGAL_ENCODER.appendEncoded(uri, fragment);
}
parseURI(uri.toString(), false);
}
/**
* Creates a new URI instance of the given unencoded component parts.
*
* @param scheme the URI scheme, or null for a non-absolute URI.
*/
public URI(String scheme, String userInfo, String host, int port, String path, String query,
String fragment) throws URISyntaxException {
if (scheme == null && userInfo == null && host == null && path == null
&& query == null && fragment == null) {
this.path = "";
return;
}
if (scheme != null && path != null && !path.isEmpty() && path.charAt(0) != '/') {
throw new URISyntaxException(path, "Relative path");
}
StringBuilder uri = new StringBuilder();
if (scheme != null) {
uri.append(scheme);
uri.append(':');
}
if (userInfo != null || host != null || port != -1) {
uri.append("//");
}
if (userInfo != null) {
USER_INFO_ENCODER.appendEncoded(uri, userInfo);
uri.append('@');
}
if (host != null) {
// check for IPv6 addresses that hasn't been enclosed in square brackets
if (host.indexOf(':') != -1 && host.indexOf(']') == -1 && host.indexOf('[') == -1) {
host = "[" + host + "]";
}
uri.append(host);
}
if (port != -1) {
uri.append(':');
uri.append(port);
}
if (path != null) {
PATH_ENCODER.appendEncoded(uri, path);
}
if (query != null) {
uri.append('?');
ALL_LEGAL_ENCODER.appendEncoded(uri, query);
}
if (fragment != null) {
uri.append('#');
ALL_LEGAL_ENCODER.appendEncoded(uri, fragment);
}
parseURI(uri.toString(), true);
}
/**
* Creates a new URI instance of the given unencoded component parts.
*
* @param scheme the URI scheme, or null for a non-absolute URI.
*/
public URI(String scheme, String host, String path, String fragment) throws URISyntaxException {
this(scheme, null, host, -1, path, null, fragment);
}
/**
* Creates a new URI instance of the given unencoded component parts.
*
* @param scheme the URI scheme, or null for a non-absolute URI.
*/
public URI(String scheme, String authority, String path, String query,
String fragment) throws URISyntaxException {
if (scheme != null && path != null && !path.isEmpty() && path.charAt(0) != '/') {
throw new URISyntaxException(path, "Relative path");
}
StringBuilder uri = new StringBuilder();
if (scheme != null) {
uri.append(scheme);
uri.append(':');
}
if (authority != null) {
uri.append("//");
AUTHORITY_ENCODER.appendEncoded(uri, authority);
}
if (path != null) {
PATH_ENCODER.appendEncoded(uri, path);
}
if (query != null) {
uri.append('?');
ALL_LEGAL_ENCODER.appendEncoded(uri, query);
}
if (fragment != null) {
uri.append('#');
ALL_LEGAL_ENCODER.appendEncoded(uri, fragment);
}
parseURI(uri.toString(), false);
}
/**
* Breaks uri into its component parts. This first splits URI into scheme,
* scheme-specific part and fragment:
* [scheme:][scheme-specific part][#fragment]
*
* Then it breaks the scheme-specific part into authority, path and query:
* [//authority][path][?query]
*
* Finally it delegates to parseAuthority to break the authority into user
* info, host and port:
* [user-info@][host][:port]
*/
private void parseURI(String uri, boolean forceServer) throws URISyntaxException {
string = uri;
// "#fragment"
int fragmentStart = UrlUtils.findFirstOf(uri, "#", 0, uri.length());
if (fragmentStart < uri.length()) {
fragment = ALL_LEGAL_ENCODER.validate(uri, fragmentStart + 1, uri.length(), "fragment");
}
// scheme:
int start;
int colon = UrlUtils.findFirstOf(uri, ":", 0, fragmentStart);
if (colon < UrlUtils.findFirstOf(uri, "/?#", 0, fragmentStart)) {
absolute = true;
scheme = validateScheme(uri, colon);
start = colon + 1;
if (start == fragmentStart) {
throw new URISyntaxException(uri, "Scheme-specific part expected", start);
}
// URIs with schemes followed by a non-/ char are opaque and need no further parsing.
if (!uri.regionMatches(start, "/", 0, 1)) {
opaque = true;
schemeSpecificPart = ALL_LEGAL_ENCODER.validate(
uri, start, fragmentStart, "scheme specific part");
return;
}
} else {
absolute = false;
start = 0;
}
opaque = false;
schemeSpecificPart = uri.substring(start, fragmentStart);
// "//authority"
int fileStart;
if (uri.regionMatches(start, "//", 0, 2)) {
int authorityStart = start + 2;
fileStart = UrlUtils.findFirstOf(uri, "/?", authorityStart, fragmentStart);
if (authorityStart == uri.length()) {
throw new URISyntaxException(uri, "Authority expected", uri.length());
}
if (authorityStart < fileStart) {
authority = AUTHORITY_ENCODER.validate(uri, authorityStart, fileStart, "authority");
}
} else {
fileStart = start;
}
// "path"
int queryStart = UrlUtils.findFirstOf(uri, "?", fileStart, fragmentStart);
path = PATH_ENCODER.validate(uri, fileStart, queryStart, "path");
// "?query"
if (queryStart < fragmentStart) {
query = ALL_LEGAL_ENCODER.validate(uri, queryStart + 1, fragmentStart, "query");
}
parseAuthority(forceServer);
}
private String validateScheme(String uri, int end) throws URISyntaxException {
if (end == 0) {
throw new URISyntaxException(uri, "Scheme expected", 0);
}
for (int i = 0; i < end; i++) {
if (!UrlUtils.isValidSchemeChar(i, uri.charAt(i))) {
throw new URISyntaxException(uri, "Illegal character in scheme", 0);
}
}
return uri.substring(0, end);
}
/**
* Breaks this URI's authority into user info, host and port parts.
* [user-info@][host][:port]
* If any part of this fails this method will give up and potentially leave
* these fields with their default values.
*
* @param forceServer true to always throw if the authority cannot be
* parsed. If false, this method may still throw for some kinds of
* errors; this unpredictable behavior is consistent with the RI.
*/
private void parseAuthority(boolean forceServer) throws URISyntaxException {
if (authority == null) {
return;
}
String tempUserInfo = null;
String temp = authority;
int index = temp.indexOf('@');
int hostIndex = 0;
if (index != -1) {
// remove user info
tempUserInfo = temp.substring(0, index);
validateUserInfo(authority, tempUserInfo, 0);
temp = temp.substring(index + 1); // host[:port] is left
hostIndex = index + 1;
}
index = temp.lastIndexOf(':');
int endIndex = temp.indexOf(']');
String tempHost;
int tempPort = -1;
if (index != -1 && endIndex < index) {
// determine port and host
tempHost = temp.substring(0, index);
if (index < (temp.length() - 1)) { // port part is not empty
try {
char firstPortChar = temp.charAt(index + 1);
if (firstPortChar >= '0' && firstPortChar <= '9') {
// allow only digits, no signs
tempPort = Integer.parseInt(temp.substring(index + 1));
} else {
if (forceServer) {
throw new URISyntaxException(authority,
"Invalid port number", hostIndex + index + 1);
}
return;
}
} catch (NumberFormatException e) {
if (forceServer) {
throw new URISyntaxException(authority,
"Invalid port number", hostIndex + index + 1);
}
return;
}
}
} else {
tempHost = temp;
}
if (tempHost.isEmpty()) {
if (forceServer) {
throw new URISyntaxException(authority, "Expected host", hostIndex);
}
return;
}
if (!isValidHost(forceServer, tempHost)) {
return;
}
// this is a server based uri,
// fill in the userInfo, host and port fields
userInfo = tempUserInfo;
host = tempHost;
port = tempPort;
serverAuthority = true;
}
private void validateUserInfo(String uri, String userInfo, int index)
throws URISyntaxException {
for (int i = 0; i < userInfo.length(); i++) {
char ch = userInfo.charAt(i);
if (ch == ']' || ch == '[') {
throw new URISyntaxException(uri, "Illegal character in userInfo", index + i);
}
}
}
/**
* Returns true if {@code host} is a well-formed host name or IP address.
*
* @param forceServer true to always throw if the host cannot be parsed. If
* false, this method may still throw for some kinds of errors; this
* unpredictable behavior is consistent with the RI.
*/
private boolean isValidHost(boolean forceServer, String host) throws URISyntaxException {
if (host.startsWith("[")) {
// IPv6 address
if (!host.endsWith("]")) {
throw new URISyntaxException(host,
"Expected a closing square bracket for IPv6 address", 0);
}
if (InetAddress.isNumeric(host)) {
// If it's numeric, the presence of square brackets guarantees
// that it's a numeric IPv6 address.
return true;
}
throw new URISyntaxException(host, "Malformed IPv6 address");
}
// '[' and ']' can only be the first char and last char
// of the host name
if (host.indexOf('[') != -1 || host.indexOf(']') != -1) {
throw new URISyntaxException(host, "Illegal character in host name", 0);
}
int index = host.lastIndexOf('.');
if (index < 0 || index == host.length() - 1
|| !Character.isDigit(host.charAt(index + 1))) {
// domain name
if (isValidDomainName(host)) {
return true;
}
if (forceServer) {
throw new URISyntaxException(host, "Illegal character in host name", 0);
}
return false;
}
// IPv4 address?
try {
InetAddress ia = InetAddress.parseNumericAddress(host);
if (ia instanceof Inet4Address) {
return true;
}
} catch (IllegalArgumentException ignored) {
}
if (forceServer) {
throw new URISyntaxException(host, "Malformed IPv4 address", 0);
}
return false;
}
private boolean isValidDomainName(String host) {
try {
// The RFCs don't permit underscores in hostnames, but URI has to because
// a certain large website doesn't seem to care about standards and specs.
// See bugs 18023709, 17579865 and 18016625.
UriCodec.validateSimple(host, "_-.");
} catch (URISyntaxException e) {
return false;
}
String lastLabel = null;
for (String token : host.split("\\.")) {
lastLabel = token;
if (lastLabel.startsWith("-") || lastLabel.endsWith("-")) {
return false;
}
}
if (lastLabel == null) {
return false;
}
if (!lastLabel.equals(host)) {
char ch = lastLabel.charAt(0);
if (ch >= '0' && ch <= '9') {
return false;
}
}
return true;
}
/**
* Compares this URI with the given argument {@code uri}. This method will
* return a negative value if this URI instance is less than the given
* argument and a positive value if this URI instance is greater than the
* given argument. The return value {@code 0} indicates that the two
* instances represent the same URI. To define the order the single parts of
* the URI are compared with each other. String components will be ordered
* in the natural case-sensitive way. A hierarchical URI is less than an
* opaque URI and if one part is {@code null} the URI with the undefined
* part is less than the other one.
*
* @param uri
* the URI this instance has to compare with.
* @return the value representing the order of the two instances.
*/
public int compareTo(URI uri) {
int ret;
// compare schemes
if (scheme == null && uri.scheme != null) {
return -1;
} else if (scheme != null && uri.scheme == null) {
return 1;
} else if (scheme != null && uri.scheme != null) {
ret = scheme.compareToIgnoreCase(uri.scheme);
if (ret != 0) {
return ret;
}
}
// compare opacities
if (!opaque && uri.opaque) {
return -1;
} else if (opaque && !uri.opaque) {
return 1;
} else if (opaque && uri.opaque) {
ret = schemeSpecificPart.compareTo(uri.schemeSpecificPart);
if (ret != 0) {
return ret;
}
} else {
// otherwise both must be hierarchical
// compare authorities
if (authority != null && uri.authority == null) {
return 1;
} else if (authority == null && uri.authority != null) {
return -1;
} else if (authority != null && uri.authority != null) {
if (host != null && uri.host != null) {
// both are server based, so compare userInfo, host, port
if (userInfo != null && uri.userInfo == null) {
return 1;
} else if (userInfo == null && uri.userInfo != null) {
return -1;
} else if (userInfo != null && uri.userInfo != null) {
ret = userInfo.compareTo(uri.userInfo);
if (ret != 0) {
return ret;
}
}
// userInfo's are the same, compare hostname
ret = host.compareToIgnoreCase(uri.host);
if (ret != 0) {
return ret;
}
// compare port
if (port != uri.port) {
return port - uri.port;
}
} else { // one or both are registry based, compare the whole
// authority
ret = authority.compareTo(uri.authority);
if (ret != 0) {
return ret;
}
}
}
// authorities are the same
// compare paths
ret = path.compareTo(uri.path);
if (ret != 0) {
return ret;
}
// compare queries
if (query != null && uri.query == null) {
return 1;
} else if (query == null && uri.query != null) {
return -1;
} else if (query != null && uri.query != null) {
ret = query.compareTo(uri.query);
if (ret != 0) {
return ret;
}
}
}
// everything else is identical, so compare fragments
if (fragment != null && uri.fragment == null) {
return 1;
} else if (fragment == null && uri.fragment != null) {
return -1;
} else if (fragment != null && uri.fragment != null) {
ret = fragment.compareTo(uri.fragment);
if (ret != 0) {
return ret;
}
}
// identical
return 0;
}
/**
* Returns the URI formed by parsing {@code uri}. This method behaves
* identically to the string constructor but throws a different exception
* on failure. The constructor fails with a checked {@link
* URISyntaxException}; this method fails with an unchecked {@link
* IllegalArgumentException}.
*/
public static URI create(String uri) {
try {
return new URI(uri);
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e.getMessage());
}
}
private URI duplicate() {
URI clone = new URI();
clone.absolute = absolute;
clone.authority = authority;
clone.fragment = fragment;
clone.host = host;
clone.opaque = opaque;
clone.path = path;
clone.port = port;
clone.query = query;
clone.scheme = scheme;
clone.schemeSpecificPart = schemeSpecificPart;
clone.userInfo = userInfo;
clone.serverAuthority = serverAuthority;
return clone;
}
/*
* Takes a string that may contain hex sequences like %F1 or %2b and
* converts the hex values following the '%' to lowercase
*/
private String convertHexToLowerCase(String s) {
StringBuilder result = new StringBuilder("");
if (s.indexOf('%') == -1) {
return s;
}
int index, prevIndex = 0;
while ((index = s.indexOf('%', prevIndex)) != -1) {
result.append(s.substring(prevIndex, index + 1));
result.append(s.substring(index + 1, index + 3).toLowerCase(Locale.US));
index += 3;
prevIndex = index;
}
return result.toString();
}
/**
* Returns true if the given URI escaped strings {@code first} and {@code second} are
* equal.
*
* TODO: This method assumes that both strings are escaped using the same escape rules
* yet it still performs case insensitive comparison of the escaped sequences.
* Why is this necessary ? We can just replace it with first.equals(second)
* otherwise.
*/
private boolean escapedEquals(String first, String second) {
// This length test isn't a micro-optimization. We need it because we sometimes
// calculate the number of characters to match based on the length of the second
// string. If the second string is shorter than the first, we might attempt to match
// 0 chars, and regionMatches is specified to return true in that case.
if (first.length() != second.length()) {
return false;
}
int prevIndex = 0;
while (true) {
int index = first.indexOf('%', prevIndex);
int index1 = second.indexOf('%', prevIndex);
if (index != index1) {
return false;
}
// index == index1 from this point on.
if (index == -1) {
// No more escapes, match the remainder of the string
// normally.
return first.regionMatches(prevIndex, second, prevIndex,
second.length() - prevIndex);
}
if (!first.regionMatches(prevIndex, second, prevIndex, (index - prevIndex))) {
return false;
}
if (!first.regionMatches(true /* ignore case */, index + 1, second, index + 1, 2)) {
return false;
}
index += 3;
prevIndex = index;
}
}
@Override public boolean equals(Object o) {
if (!(o instanceof URI)) {
return false;
}
URI uri = (URI) o;
if (uri.fragment == null && fragment != null || uri.fragment != null
&& fragment == null) {
return false;
} else if (uri.fragment != null && fragment != null) {
if (!escapedEquals(uri.fragment, fragment)) {
return false;
}
}
if (uri.scheme == null && scheme != null || uri.scheme != null
&& scheme == null) {
return false;
} else if (uri.scheme != null && scheme != null) {
if (!uri.scheme.equalsIgnoreCase(scheme)) {
return false;
}
}
if (uri.opaque && opaque) {
return escapedEquals(uri.schemeSpecificPart,
schemeSpecificPart);
} else if (!uri.opaque && !opaque) {
if (!escapedEquals(path, uri.path)) {
return false;
}
if (uri.query != null && query == null || uri.query == null
&& query != null) {
return false;
} else if (uri.query != null && query != null) {
if (!escapedEquals(uri.query, query)) {
return false;
}
}
if (uri.authority != null && authority == null
|| uri.authority == null && authority != null) {
return false;
} else if (uri.authority != null && authority != null) {
if (uri.host != null && host == null || uri.host == null
&& host != null) {
return false;
} else if (uri.host == null && host == null) {
// both are registry based, so compare the whole authority
return escapedEquals(uri.authority, authority);
} else { // uri.host != null && host != null, so server-based
if (!host.equalsIgnoreCase(uri.host)) {
return false;
}
if (port != uri.port) {
return false;
}
if (uri.userInfo != null && userInfo == null
|| uri.userInfo == null && userInfo != null) {
return false;
} else if (uri.userInfo != null && userInfo != null) {
return escapedEquals(userInfo, uri.userInfo);
} else {
return true;
}
}
} else {
// no authority
return true;
}
} else {
// one is opaque, the other hierarchical
return false;
}
}
/**
* Returns the scheme of this URI, or null if this URI has no scheme. This
* is also known as the protocol.
*/
public String getScheme() {
return scheme;
}
/**
* Returns the decoded scheme-specific part of this URI, or null if this URI
* has no scheme-specific part.
*/
public String getSchemeSpecificPart() {
return decode(schemeSpecificPart);
}
/**
* Returns the encoded scheme-specific part of this URI, or null if this URI
* has no scheme-specific part.
*/
public String getRawSchemeSpecificPart() {
return schemeSpecificPart;
}
/**
* Returns the decoded authority part of this URI, or null if this URI has
* no authority.
*/
public String getAuthority() {
return decode(authority);
}
/**
* Returns the encoded authority of this URI, or null if this URI has no
* authority.
*/
public String getRawAuthority() {
return authority;
}
/**
* Returns the decoded user info of this URI, or null if this URI has no
* user info.
*/
public String getUserInfo() {
return decode(userInfo);
}
/**
* Returns the encoded user info of this URI, or null if this URI has no
* user info.
*/
public String getRawUserInfo() {
return userInfo;
}
/**
* Returns the host of this URI, or null if this URI has no host.
*/
public String getHost() {
return host;
}
/**
* Returns the port number of this URI, or {@code -1} if this URI has no
* explicit port.
*/
public int getPort() {
return port;
}
/** @hide */
public int getEffectivePort() {
return getEffectivePort(scheme, port);
}
/**
* Returns the port to use for {@code scheme} connections will use when
* {@link #getPort} returns {@code specifiedPort}.
*
* @hide
*/
public static int getEffectivePort(String scheme, int specifiedPort) {
if (specifiedPort != -1) {
return specifiedPort;
}
if ("http".equalsIgnoreCase(scheme)) {
return 80;
} else if ("https".equalsIgnoreCase(scheme)) {
return 443;
} else {
return -1;
}
}
/**
* Returns the decoded path of this URI, or null if this URI has no path.
*/
public String getPath() {
return decode(path);
}
/**
* Returns the encoded path of this URI, or null if this URI has no path.
*/
public String getRawPath() {
return path;
}
/**
* Returns the decoded query of this URI, or null if this URI has no query.
*/
public String getQuery() {
return decode(query);
}
/**
* Returns the encoded query of this URI, or null if this URI has no query.
*/
public String getRawQuery() {
return query;
}
/**
* Returns the decoded fragment of this URI, or null if this URI has no
* fragment.
*/
public String getFragment() {
return decode(fragment);
}
/**
* Gets the encoded fragment of this URI, or null if this URI has no
* fragment.
*/
public String getRawFragment() {
return fragment;
}
@Override public int hashCode() {
if (hash == -1) {
hash = getHashString().hashCode();
}
return hash;
}
/**
* Returns true if this URI is absolute, which means that a scheme is
* defined.
*/
public boolean isAbsolute() {
// TODO: simplify to 'scheme != null' ?
return absolute;
}
/**
* Returns true if this URI is opaque. Opaque URIs are absolute and have a
* scheme-specific part that does not start with a slash character. All
* parts except scheme, scheme-specific and fragment are undefined.
*/
public boolean isOpaque() {
return opaque;
}
/**
* Returns the normalized path.
*/
private String normalize(String path, boolean discardRelativePrefix) {
path = UrlUtils.canonicalizePath(path, discardRelativePrefix);
/*
* If the path contains a colon before the first colon, prepend
* "./" to differentiate the path from a scheme prefix.
*/
int colon = path.indexOf(':');
if (colon != -1) {
int slash = path.indexOf('/');
if (slash == -1 || colon < slash) {
path = "./" + path;
}
}
return path;
}
/**
* Normalizes the path part of this URI.
*
* @return an URI object which represents this instance with a normalized
* path.
*/
public URI normalize() {
if (opaque) {
return this;
}
String normalizedPath = normalize(path, false);
// if the path is already normalized, return this
if (path.equals(normalizedPath)) {
return this;
}
// get an exact copy of the URI re-calculate the scheme specific part
// since the path of the normalized URI is different from this URI.
URI result = duplicate();
result.path = normalizedPath;
result.setSchemeSpecificPart();
return result;
}
/**
* Tries to parse the authority component of this URI to divide it into the
* host, port, and user-info. If this URI is already determined as a
* ServerAuthority this instance will be returned without changes.
*
* @return this instance with the components of the parsed server authority.
* @throws URISyntaxException
* if the authority part could not be parsed as a server-based
* authority.
*/
public URI parseServerAuthority() throws URISyntaxException {
if (!serverAuthority) {
parseAuthority(true);
}
return this;
}
/**
* Makes the given URI {@code relative} to a relative URI against the URI
* represented by this instance.
*
* @param relative
* the URI which has to be relativized against this URI.
* @return the relative URI.
*/
public URI relativize(URI relative) {
if (relative.opaque || opaque) {
return relative;
}
if (scheme == null ? relative.scheme != null : !scheme
.equals(relative.scheme)) {
return relative;
}
if (authority == null ? relative.authority != null : !authority
.equals(relative.authority)) {
return relative;
}
// normalize both paths
String thisPath = normalize(path, false);
String relativePath = normalize(relative.path, false);
/*
* if the paths aren't equal, then we need to determine if this URI's
* path is a parent path (begins with) the relative URI's path
*/
if (!thisPath.equals(relativePath)) {
// drop everything after the last slash in this path
thisPath = thisPath.substring(0, thisPath.lastIndexOf('/') + 1);
/*
* if the relative URI's path doesn't start with this URI's path,
* then just return the relative URI; the URIs have nothing in
* common
*/
if (!relativePath.startsWith(thisPath)) {
return relative;
}
}
URI result = new URI();
result.fragment = relative.fragment;
result.query = relative.query;
// the result URI is the remainder of the relative URI's path
result.path = relativePath.substring(thisPath.length());
result.setSchemeSpecificPart();
return result;
}
/**
* Resolves the given URI {@code relative} against the URI represented by
* this instance.
*
* @param relative
* the URI which has to be resolved against this URI.
* @return the resolved URI.
*/
public URI resolve(URI relative) {
if (relative.absolute || opaque) {
return relative;
}
if (relative.authority != null) {
// If the relative URI has an authority, the result is the relative
// with this URI's scheme.
URI result = relative.duplicate();
result.scheme = scheme;
result.absolute = absolute;
return result;
}
if (relative.path.isEmpty() && relative.scheme == null && relative.query == null) {
// if the relative URI only consists of at most a fragment,
URI result = duplicate();
result.fragment = relative.fragment;
return result;
}
URI result = duplicate();
result.fragment = relative.fragment;
result.query = relative.query;
String resolvedPath;
if (relative.path.startsWith("/")) {
// The relative URI has an absolute path; use it.
resolvedPath = relative.path;
} else if (relative.path.isEmpty()) {
// The relative URI has no path; use the base path.
resolvedPath = path;
} else {
// The relative URI has a relative path; combine the paths.
int endIndex = path.lastIndexOf('/') + 1;
resolvedPath = path.substring(0, endIndex) + relative.path;
}
result.path = UrlUtils.authoritySafePath(result.authority, normalize(resolvedPath, true));
result.setSchemeSpecificPart();
return result;
}
/**
* Helper method used to re-calculate the scheme specific part of the
* resolved or normalized URIs
*/
private void setSchemeSpecificPart() {
// ssp = [//authority][path][?query]
StringBuilder ssp = new StringBuilder();
if (authority != null) {
ssp.append("//" + authority);
}
if (path != null) {
ssp.append(path);
}
if (query != null) {
ssp.append("?" + query);
}
schemeSpecificPart = ssp.toString();
// reset string, so that it can be re-calculated correctly when asked.
string = null;
}
/**
* Creates a new URI instance by parsing the given string {@code relative}
* and resolves the created URI against the URI represented by this
* instance.
*
* @param relative
* the given string to create the new URI instance which has to
* be resolved later on.
* @return the created and resolved URI.
*/
public URI resolve(String relative) {
return resolve(create(relative));
}
private String decode(String s) {
return s != null ? UriCodec.decode(s) : null;
}
/**
* Returns the textual string representation of this URI instance using the
* US-ASCII encoding.
*
* @return the US-ASCII string representation of this URI.
*/
public String toASCIIString() {
StringBuilder result = new StringBuilder();
ASCII_ONLY.appendEncoded(result, toString());
return result.toString();
}
/**
* Returns the encoded URI.
*/
@Override public String toString() {
if (string != null) {
return string;
}
StringBuilder result = new StringBuilder();
if (scheme != null) {
result.append(scheme);
result.append(':');
}
if (opaque) {
result.append(schemeSpecificPart);
} else {
if (authority != null) {
result.append("//");
result.append(authority);
}
if (path != null) {
result.append(path);
}
if (query != null) {
result.append('?');
result.append(query);
}
}
if (fragment != null) {
result.append('#');
result.append(fragment);
}
string = result.toString();
return string;
}
/*
* Form a string from the components of this URI, similarly to the
* toString() method. But this method converts scheme and host to lowercase,
* and converts escaped octets to lowercase.
*/
private String getHashString() {
StringBuilder result = new StringBuilder();
if (scheme != null) {
result.append(scheme.toLowerCase(Locale.US));
result.append(':');
}
if (opaque) {
result.append(schemeSpecificPart);
} else {
if (authority != null) {
result.append("//");
if (host == null) {
result.append(authority);
} else {
if (userInfo != null) {
result.append(userInfo + "@");
}
result.append(host.toLowerCase(Locale.US));
if (port != -1) {
result.append(":" + port);
}
}
}
if (path != null) {
result.append(path);
}
if (query != null) {
result.append('?');
result.append(query);
}
}
if (fragment != null) {
result.append('#');
result.append(fragment);
}
return convertHexToLowerCase(result.toString());
}
/**
* Converts this URI instance to a URL.
*
* @return the created URL representing the same resource as this URI.
* @throws MalformedURLException
* if an error occurs while creating the URL or no protocol
* handler could be found.
*/
public URL toURL() throws MalformedURLException {
if (!absolute) {
throw new IllegalArgumentException("URI is not absolute: " + toString());
}
return new URL(toString());
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
try {
parseURI(string, false);
} catch (URISyntaxException e) {
throw new IOException(e.toString());
}
}
private void writeObject(ObjectOutputStream out) throws IOException, ClassNotFoundException {
// call toString() to ensure the value of string field is calculated
toString();
out.defaultWriteObject();
}
}