Home | History | Annotate | Download | only in net
      1 /*
      2  *  Licensed to the Apache Software Foundation (ASF) under one or more
      3  *  contributor license agreements.  See the NOTICE file distributed with
      4  *  this work for additional information regarding copyright ownership.
      5  *  The ASF licenses this file to You under the Apache License, Version 2.0
      6  *  (the "License"); you may not use this file except in compliance with
      7  *  the License.  You may obtain a copy of the License at
      8  *
      9  *     http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  *  Unless required by applicable law or agreed to in writing, software
     12  *  distributed under the License is distributed on an "AS IS" BASIS,
     13  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  *  See the License for the specific language governing permissions and
     15  *  limitations under the License.
     16  */
     17 
     18 package java.net;
     19 
     20 import java.io.IOException;
     21 import java.io.ObjectInputStream;
     22 import java.io.ObjectOutputStream;
     23 import java.io.Serializable;
     24 import java.io.UnsupportedEncodingException;
     25 import java.util.StringTokenizer;
     26 import org.apache.harmony.luni.platform.INetworkSystem;
     27 import org.apache.harmony.luni.platform.Platform;
     28 
     29 /**
     30  * This class represents an instance of a URI as defined by RFC 2396.
     31  */
     32 public final class URI implements Comparable<URI>, Serializable {
     33 
     34     private final static INetworkSystem NETWORK_SYSTEM = Platform.getNetworkSystem();
     35 
     36     private static final long serialVersionUID = -6052424284110960213l;
     37 
     38     static final String UNRESERVED = "_-!.~\'()*";
     39     static final String PUNCTUATION = ",;:$&+=";
     40     static final String RESERVED = PUNCTUATION + "?/[]@";
     41     static final String SOME_LEGAL = UNRESERVED + PUNCTUATION;
     42     static final String ALL_LEGAL = UNRESERVED + RESERVED;
     43 
     44     private String string;
     45     private transient String scheme;
     46     private transient String schemeSpecificPart;
     47     private transient String authority;
     48     private transient String userInfo;
     49     private transient String host;
     50     private transient int port = -1;
     51     private transient String path;
     52     private transient String query;
     53     private transient String fragment;
     54     private transient boolean opaque;
     55     private transient boolean absolute;
     56     private transient boolean serverAuthority = false;
     57 
     58     private transient int hash = -1;
     59 
     60     private URI() {}
     61 
     62     /**
     63      * Creates a new URI instance according to the given string {@code uri}.
     64      *
     65      * @param uri
     66      *            the textual URI representation to be parsed into a URI object.
     67      * @throws URISyntaxException
     68      *             if the given string {@code uri} doesn't fit to the
     69      *             specification RFC2396 or could not be parsed correctly.
     70      */
     71     public URI(String uri) throws URISyntaxException {
     72         parseURI(uri, false);
     73     }
     74 
     75     /**
     76      * Creates a new URI instance using the given arguments. This constructor
     77      * first creates a temporary URI string from the given components. This
     78      * string will be parsed later on to create the URI instance.
     79      * <p>
     80      * {@code [scheme:]scheme-specific-part[#fragment]}
     81      *
     82      * @param scheme
     83      *            the scheme part of the URI.
     84      * @param ssp
     85      *            the scheme-specific-part of the URI.
     86      * @param frag
     87      *            the fragment part of the URI.
     88      * @throws URISyntaxException
     89      *             if the temporary created string doesn't fit to the
     90      *             specification RFC2396 or could not be parsed correctly.
     91      */
     92     public URI(String scheme, String ssp, String frag)
     93             throws URISyntaxException {
     94         StringBuilder uri = new StringBuilder();
     95         if (scheme != null) {
     96             uri.append(scheme);
     97             uri.append(':');
     98         }
     99         if (ssp != null) {
    100             // QUOTE ILLEGAL CHARACTERS
    101             uri.append(quoteComponent(ssp, ALL_LEGAL));
    102         }
    103         if (frag != null) {
    104             uri.append('#');
    105             // QUOTE ILLEGAL CHARACTERS
    106             uri.append(quoteComponent(frag, ALL_LEGAL));
    107         }
    108 
    109         parseURI(uri.toString(), false);
    110     }
    111 
    112     /**
    113      * Creates a new URI instance using the given arguments. This constructor
    114      * first creates a temporary URI string from the given components. This
    115      * string will be parsed later on to create the URI instance.
    116      * <p>
    117      * {@code [scheme:][user-info@]host[:port][path][?query][#fragment]}
    118      *
    119      * @param scheme
    120      *            the scheme part of the URI.
    121      * @param userInfo
    122      *            the user information of the URI for authentication and
    123      *            authorization.
    124      * @param host
    125      *            the host name of the URI.
    126      * @param port
    127      *            the port number of the URI.
    128      * @param path
    129      *            the path to the resource on the host.
    130      * @param query
    131      *            the query part of the URI to specify parameters for the
    132      *            resource.
    133      * @param fragment
    134      *            the fragment part of the URI.
    135      * @throws URISyntaxException
    136      *             if the temporary created string doesn't fit to the
    137      *             specification RFC2396 or could not be parsed correctly.
    138      */
    139     public URI(String scheme, String userInfo, String host, int port,
    140             String path, String query, String fragment)
    141             throws URISyntaxException {
    142 
    143         if (scheme == null && userInfo == null && host == null && path == null
    144                 && query == null && fragment == null) {
    145             this.path = "";
    146             return;
    147         }
    148 
    149         if (scheme != null && path != null && path.length() > 0
    150                 && path.charAt(0) != '/') {
    151             throw new URISyntaxException(path, "Relative path");
    152         }
    153 
    154         StringBuilder uri = new StringBuilder();
    155         if (scheme != null) {
    156             uri.append(scheme);
    157             uri.append(':');
    158         }
    159 
    160         if (userInfo != null || host != null || port != -1) {
    161             uri.append("//");
    162         }
    163 
    164         if (userInfo != null) {
    165             // QUOTE ILLEGAL CHARACTERS in userInfo
    166             uri.append(quoteComponent(userInfo, SOME_LEGAL));
    167             uri.append('@');
    168         }
    169 
    170         if (host != null) {
    171             // check for IPv6 addresses that hasn't been enclosed
    172             // in square brackets
    173             if (host.indexOf(':') != -1 && host.indexOf(']') == -1
    174                     && host.indexOf('[') == -1) {
    175                 host = "[" + host + "]";
    176             }
    177             uri.append(host);
    178         }
    179 
    180         if (port != -1) {
    181             uri.append(':');
    182             uri.append(port);
    183         }
    184 
    185         if (path != null) {
    186             // QUOTE ILLEGAL CHARS
    187             uri.append(quoteComponent(path, "/@" + SOME_LEGAL));
    188         }
    189 
    190         if (query != null) {
    191             uri.append('?');
    192             // QUOTE ILLEGAL CHARS
    193             uri.append(quoteComponent(query, ALL_LEGAL));
    194         }
    195 
    196         if (fragment != null) {
    197             // QUOTE ILLEGAL CHARS
    198             uri.append('#');
    199             uri.append(quoteComponent(fragment, ALL_LEGAL));
    200         }
    201 
    202         parseURI(uri.toString(), true);
    203     }
    204 
    205     /**
    206      * Creates a new URI instance using the given arguments. This constructor
    207      * first creates a temporary URI string from the given components. This
    208      * string will be parsed later on to create the URI instance.
    209      * <p>
    210      * {@code [scheme:]host[path][#fragment]}
    211      *
    212      * @param scheme
    213      *            the scheme part of the URI.
    214      * @param host
    215      *            the host name of the URI.
    216      * @param path
    217      *            the path to the resource on the host.
    218      * @param fragment
    219      *            the fragment part of the URI.
    220      * @throws URISyntaxException
    221      *             if the temporary created string doesn't fit to the
    222      *             specification RFC2396 or could not be parsed correctly.
    223      */
    224     public URI(String scheme, String host, String path, String fragment)
    225             throws URISyntaxException {
    226         this(scheme, null, host, -1, path, null, fragment);
    227     }
    228 
    229     /**
    230      * Creates a new URI instance using the given arguments. This constructor
    231      * first creates a temporary URI string from the given components. This
    232      * string will be parsed later on to create the URI instance.
    233      * <p>
    234      * {@code [scheme:][//authority][path][?query][#fragment]}
    235      *
    236      * @param scheme
    237      *            the scheme part of the URI.
    238      * @param authority
    239      *            the authority part of the URI.
    240      * @param path
    241      *            the path to the resource on the host.
    242      * @param query
    243      *            the query part of the URI to specify parameters for the
    244      *            resource.
    245      * @param fragment
    246      *            the fragment part of the URI.
    247      * @throws URISyntaxException
    248      *             if the temporary created string doesn't fit to the
    249      *             specification RFC2396 or could not be parsed correctly.
    250      */
    251     public URI(String scheme, String authority, String path, String query,
    252             String fragment) throws URISyntaxException {
    253         if (scheme != null && path != null && path.length() > 0
    254                 && path.charAt(0) != '/') {
    255             throw new URISyntaxException(path, "Relative path");
    256         }
    257 
    258         StringBuilder uri = new StringBuilder();
    259         if (scheme != null) {
    260             uri.append(scheme);
    261             uri.append(':');
    262         }
    263         if (authority != null) {
    264             uri.append("//");
    265             // QUOTE ILLEGAL CHARS
    266             uri.append(quoteComponent(authority, "@[]" + SOME_LEGAL));
    267         }
    268 
    269         if (path != null) {
    270             // QUOTE ILLEGAL CHARS
    271             uri.append(quoteComponent(path, "/@" + SOME_LEGAL));
    272         }
    273         if (query != null) {
    274             // QUOTE ILLEGAL CHARS
    275             uri.append('?');
    276             uri.append(quoteComponent(query, ALL_LEGAL));
    277         }
    278         if (fragment != null) {
    279             // QUOTE ILLEGAL CHARS
    280             uri.append('#');
    281             uri.append(quoteComponent(fragment, ALL_LEGAL));
    282         }
    283 
    284         parseURI(uri.toString(), false);
    285     }
    286 
    287     private void parseURI(String uri, boolean forceServer) throws URISyntaxException {
    288         String temp = uri;
    289         // assign uri string to the input value per spec
    290         string = uri;
    291         int index, index1, index2, index3;
    292         // parse into Fragment, Scheme, and SchemeSpecificPart
    293         // then parse SchemeSpecificPart if necessary
    294 
    295         // Fragment
    296         index = temp.indexOf('#');
    297         if (index != -1) {
    298             // remove the fragment from the end
    299             fragment = temp.substring(index + 1);
    300             validateFragment(uri, fragment, index + 1);
    301             temp = temp.substring(0, index);
    302         }
    303 
    304         // Scheme and SchemeSpecificPart
    305         index = index1 = temp.indexOf(':');
    306         index2 = temp.indexOf('/');
    307         index3 = temp.indexOf('?');
    308 
    309         // if a '/' or '?' occurs before the first ':' the uri has no
    310         // specified scheme, and is therefore not absolute
    311         if (index != -1 && (index2 >= index || index2 == -1)
    312                 && (index3 >= index || index3 == -1)) {
    313             // the characters up to the first ':' comprise the scheme
    314             absolute = true;
    315             scheme = temp.substring(0, index);
    316             if (scheme.length() == 0) {
    317                 throw new URISyntaxException(uri, "Scheme expected", index);
    318             }
    319             validateScheme(uri, scheme, 0);
    320             schemeSpecificPart = temp.substring(index + 1);
    321             if (schemeSpecificPart.length() == 0) {
    322                 throw new URISyntaxException(uri, "Scheme-specific part expected", index + 1);
    323             }
    324         } else {
    325             absolute = false;
    326             schemeSpecificPart = temp;
    327         }
    328 
    329         if (scheme == null || schemeSpecificPart.length() > 0
    330                 && schemeSpecificPart.charAt(0) == '/') {
    331             opaque = false;
    332             // the URI is hierarchical
    333 
    334             // Query
    335             temp = schemeSpecificPart;
    336             index = temp.indexOf('?');
    337             if (index != -1) {
    338                 query = temp.substring(index + 1);
    339                 temp = temp.substring(0, index);
    340                 validateQuery(uri, query, index2 + 1 + index);
    341             }
    342 
    343             // Authority and Path
    344             if (temp.startsWith("//")) {
    345                 index = temp.indexOf('/', 2);
    346                 if (index != -1) {
    347                     authority = temp.substring(2, index);
    348                     path = temp.substring(index);
    349                 } else {
    350                     authority = temp.substring(2);
    351                     if (authority.length() == 0 && query == null
    352                             && fragment == null) {
    353                         throw new URISyntaxException(uri, "Authority expected", uri.length());
    354                     }
    355 
    356                     path = "";
    357                     // nothing left, so path is empty (not null, path should
    358                     // never be null)
    359                 }
    360 
    361                 if (authority.length() == 0) {
    362                     authority = null;
    363                 } else {
    364                     validateAuthority(uri, authority, index1 + 3);
    365                 }
    366             } else { // no authority specified
    367                 path = temp;
    368             }
    369 
    370             int pathIndex = 0;
    371             if (index2 > -1) {
    372                 pathIndex += index2;
    373             }
    374             if (index > -1) {
    375                 pathIndex += index;
    376             }
    377             validatePath(uri, path, pathIndex);
    378         } else { // if not hierarchical, URI is opaque
    379             opaque = true;
    380             validateSsp(uri, schemeSpecificPart, index2 + 2 + index);
    381         }
    382 
    383         parseAuthority(forceServer);
    384     }
    385 
    386     private void validateScheme(String uri, String scheme, int index)
    387             throws URISyntaxException {
    388         // first char needs to be an alpha char
    389         char ch = scheme.charAt(0);
    390         if (!((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'))) {
    391             throw new URISyntaxException(uri, "Illegal character in scheme", 0);
    392         }
    393 
    394         try {
    395             URIEncoderDecoder.validateSimple(scheme, "+-.");
    396         } catch (URISyntaxException e) {
    397             throw new URISyntaxException(uri, "Illegal character in scheme", index + e.getIndex());
    398         }
    399     }
    400 
    401     private void validateSsp(String uri, String ssp, int index)
    402             throws URISyntaxException {
    403         try {
    404             URIEncoderDecoder.validate(ssp, ALL_LEGAL);
    405         } catch (URISyntaxException e) {
    406             throw new URISyntaxException(uri,
    407                     e.getReason() + " in schemeSpecificPart", index + e.getIndex());
    408         }
    409     }
    410 
    411     private void validateAuthority(String uri, String authority, int index)
    412             throws URISyntaxException {
    413         try {
    414             URIEncoderDecoder.validate(authority, "@[]" + SOME_LEGAL);
    415         } catch (URISyntaxException e) {
    416             throw new URISyntaxException(uri, e.getReason() + " in authority", index + e.getIndex());
    417         }
    418     }
    419 
    420     private void validatePath(String uri, String path, int index)
    421             throws URISyntaxException {
    422         try {
    423             URIEncoderDecoder.validate(path, "/@" + SOME_LEGAL);
    424         } catch (URISyntaxException e) {
    425             throw new URISyntaxException(uri, e.getReason() + " in path", index + e.getIndex());
    426         }
    427     }
    428 
    429     private void validateQuery(String uri, String query, int index)
    430             throws URISyntaxException {
    431         try {
    432             URIEncoderDecoder.validate(query, ALL_LEGAL);
    433         } catch (URISyntaxException e) {
    434             throw new URISyntaxException(uri, e.getReason() + " in query", index + e.getIndex());
    435 
    436         }
    437     }
    438 
    439     private void validateFragment(String uri, String fragment, int index)
    440             throws URISyntaxException {
    441         try {
    442             URIEncoderDecoder.validate(fragment, ALL_LEGAL);
    443         } catch (URISyntaxException e) {
    444             throw new URISyntaxException(uri, e.getReason() + " in fragment", index + e.getIndex());
    445         }
    446     }
    447 
    448     /**
    449      * Parse the authority string into its component parts: user info,
    450      * host, and port. This operation doesn't apply to registry URIs, and
    451      * calling it on such <i>may</i> result in a syntax exception.
    452      *
    453      * @param forceServer true to always throw if the authority cannot be
    454      *     parsed. If false, this method may still throw for some kinds of
    455      *     errors; this unpredictable behaviour is consistent with the RI.
    456      */
    457     private void parseAuthority(boolean forceServer) throws URISyntaxException {
    458         if (authority == null) {
    459             return;
    460         }
    461 
    462         String tempUserInfo = null;
    463         String temp = authority;
    464         int index = temp.indexOf('@');
    465         int hostIndex = 0;
    466         if (index != -1) {
    467             // remove user info
    468             tempUserInfo = temp.substring(0, index);
    469             validateUserInfo(authority, tempUserInfo, 0);
    470             temp = temp.substring(index + 1); // host[:port] is left
    471             hostIndex = index + 1;
    472         }
    473 
    474         index = temp.lastIndexOf(':');
    475         int endIndex = temp.indexOf(']');
    476 
    477         String tempHost;
    478         int tempPort = -1;
    479         if (index != -1 && endIndex < index) {
    480             // determine port and host
    481             tempHost = temp.substring(0, index);
    482 
    483             if (index < (temp.length() - 1)) { // port part is not empty
    484                 try {
    485                     tempPort = Integer.parseInt(temp.substring(index + 1));
    486                     if (tempPort < 0) {
    487                         if (forceServer) {
    488                             throw new URISyntaxException(authority,
    489                                     "Invalid port number", hostIndex + index + 1);
    490                         }
    491                         return;
    492                     }
    493                 } catch (NumberFormatException e) {
    494                     if (forceServer) {
    495                         throw new URISyntaxException(authority,
    496                                 "Invalid port number", hostIndex + index + 1);
    497                     }
    498                     return;
    499                 }
    500             }
    501         } else {
    502             tempHost = temp;
    503         }
    504 
    505         if (tempHost.isEmpty()) {
    506             if (forceServer) {
    507                 throw new URISyntaxException(authority, "Expected host", hostIndex);
    508             }
    509             return;
    510         }
    511 
    512         if (!isValidHost(forceServer, tempHost)) {
    513             return;
    514         }
    515 
    516         // this is a server based uri,
    517         // fill in the userInfo, host and port fields
    518         userInfo = tempUserInfo;
    519         host = tempHost;
    520         port = tempPort;
    521         serverAuthority = true;
    522     }
    523 
    524     private void validateUserInfo(String uri, String userInfo, int index)
    525             throws URISyntaxException {
    526         for (int i = 0; i < userInfo.length(); i++) {
    527             char ch = userInfo.charAt(i);
    528             if (ch == ']' || ch == '[') {
    529                 throw new URISyntaxException(uri, "Illegal character in userInfo", index + i);
    530             }
    531         }
    532     }
    533 
    534     /**
    535      * Returns true if {@code host} is a well-formed host name or IP address.
    536      *
    537      * @param forceServer true to always throw if the host cannot be parsed. If
    538      *     false, this method may still throw for some kinds of errors; this
    539      *     unpredictable behaviour is consistent with the RI.
    540      */
    541     private boolean isValidHost(boolean forceServer, String host) throws URISyntaxException {
    542         if (host.startsWith("[")) {
    543             // IPv6 address
    544             if (!host.endsWith("]")) {
    545                 throw new URISyntaxException(host,
    546                         "Expected a closing square bracket for IPv6 address", 0);
    547             }
    548             try {
    549                 byte[] bytes = InetAddress.ipStringToByteArray(host);
    550                 /*
    551                  * The native IP parser may return 4 bytes for addresses like
    552                  * "[::FFFF:127.0.0.1]". This is allowed, but we must not accept
    553                  * IPv4-formatted addresses in square braces like "[127.0.0.1]".
    554                  */
    555                 if (bytes.length == 16 || bytes.length == 4 && host.contains(":")) {
    556                     return true;
    557                 }
    558             } catch (UnknownHostException e) {
    559             }
    560             throw new URISyntaxException(host, "Malformed IPv6 address");
    561         }
    562 
    563         // '[' and ']' can only be the first char and last char
    564         // of the host name
    565         if (host.indexOf('[') != -1 || host.indexOf(']') != -1) {
    566             throw new URISyntaxException(host, "Illegal character in host name", 0);
    567         }
    568 
    569         int index = host.lastIndexOf('.');
    570         if (index < 0 || index == host.length() - 1
    571                 || !Character.isDigit(host.charAt(index + 1))) {
    572             // domain name
    573             if (isValidDomainName(host)) {
    574                 return true;
    575             }
    576             if (forceServer) {
    577                 throw new URISyntaxException(host, "Illegal character in host name", 0);
    578             }
    579             return false;
    580         }
    581 
    582         // IPv4 address
    583         try {
    584             if (InetAddress.ipStringToByteArray(host).length == 4) {
    585                 return true;
    586             }
    587         } catch (UnknownHostException e) {
    588         }
    589 
    590         if (forceServer) {
    591             throw new URISyntaxException(host, "Malformed IPv4 address", 0);
    592         }
    593         return false;
    594     }
    595 
    596     private boolean isValidDomainName(String host) {
    597         try {
    598             URIEncoderDecoder.validateSimple(host, "-.");
    599         } catch (URISyntaxException e) {
    600             return false;
    601         }
    602 
    603         String lastLabel = null;
    604         StringTokenizer st = new StringTokenizer(host, ".");
    605         while (st.hasMoreTokens()) {
    606             lastLabel = st.nextToken();
    607             if (lastLabel.startsWith("-") || lastLabel.endsWith("-")) {
    608                 return false;
    609             }
    610         }
    611 
    612         if (lastLabel == null) {
    613             return false;
    614         }
    615 
    616         if (!lastLabel.equals(host)) {
    617             char ch = lastLabel.charAt(0);
    618             if (ch >= '0' && ch <= '9') {
    619                 return false;
    620             }
    621         }
    622         return true;
    623     }
    624 
    625     /**
    626      * Quote illegal chars for each component, but not the others
    627      *
    628      * @param component java.lang.String the component to be converted
    629      * @param legalSet the legal character set allowed in the component
    630      * @return java.lang.String the converted string
    631      */
    632     private String quoteComponent(String component, String legalSet) {
    633         try {
    634             /*
    635              * Use a different encoder than URLEncoder since: 1. chars like "/",
    636              * "#", "@" etc needs to be preserved instead of being encoded, 2.
    637              * UTF-8 char set needs to be used for encoding instead of default
    638              * platform one
    639              */
    640             return URIEncoderDecoder.quoteIllegal(component, legalSet);
    641         } catch (UnsupportedEncodingException e) {
    642             throw new RuntimeException(e.toString());
    643         }
    644     }
    645 
    646     /**
    647      * Compares this URI with the given argument {@code uri}. This method will
    648      * return a negative value if this URI instance is less than the given
    649      * argument and a positive value if this URI instance is greater than the
    650      * given argument. The return value {@code 0} indicates that the two
    651      * instances represent the same URI. To define the order the single parts of
    652      * the URI are compared with each other. String components will be ordered
    653      * in the natural case-sensitive way. A hierarchical URI is less than an
    654      * opaque URI and if one part is {@code null} the URI with the undefined
    655      * part is less than the other one.
    656      *
    657      * @param uri
    658      *            the URI this instance has to compare with.
    659      * @return the value representing the order of the two instances.
    660      */
    661     public int compareTo(URI uri) {
    662         int ret;
    663 
    664         // compare schemes
    665         if (scheme == null && uri.scheme != null) {
    666             return -1;
    667         } else if (scheme != null && uri.scheme == null) {
    668             return 1;
    669         } else if (scheme != null && uri.scheme != null) {
    670             ret = scheme.compareToIgnoreCase(uri.scheme);
    671             if (ret != 0) {
    672                 return ret;
    673             }
    674         }
    675 
    676         // compare opacities
    677         if (!opaque && uri.opaque) {
    678             return -1;
    679         } else if (opaque && !uri.opaque) {
    680             return 1;
    681         } else if (opaque && uri.opaque) {
    682             ret = schemeSpecificPart.compareTo(uri.schemeSpecificPart);
    683             if (ret != 0) {
    684                 return ret;
    685             }
    686         } else {
    687 
    688             // otherwise both must be hierarchical
    689 
    690             // compare authorities
    691             if (authority != null && uri.authority == null) {
    692                 return 1;
    693             } else if (authority == null && uri.authority != null) {
    694                 return -1;
    695             } else if (authority != null && uri.authority != null) {
    696                 if (host != null && uri.host != null) {
    697                     // both are server based, so compare userInfo, host, port
    698                     if (userInfo != null && uri.userInfo == null) {
    699                         return 1;
    700                     } else if (userInfo == null && uri.userInfo != null) {
    701                         return -1;
    702                     } else if (userInfo != null && uri.userInfo != null) {
    703                         ret = userInfo.compareTo(uri.userInfo);
    704                         if (ret != 0) {
    705                             return ret;
    706                         }
    707                     }
    708 
    709                     // userInfo's are the same, compare hostname
    710                     ret = host.compareToIgnoreCase(uri.host);
    711                     if (ret != 0) {
    712                         return ret;
    713                     }
    714 
    715                     // compare port
    716                     if (port != uri.port) {
    717                         return port - uri.port;
    718                     }
    719                 } else { // one or both are registry based, compare the whole
    720                     // authority
    721                     ret = authority.compareTo(uri.authority);
    722                     if (ret != 0) {
    723                         return ret;
    724                     }
    725                 }
    726             }
    727 
    728             // authorities are the same
    729             // compare paths
    730             ret = path.compareTo(uri.path);
    731             if (ret != 0) {
    732                 return ret;
    733             }
    734 
    735             // compare queries
    736 
    737             if (query != null && uri.query == null) {
    738                 return 1;
    739             } else if (query == null && uri.query != null) {
    740                 return -1;
    741             } else if (query != null && uri.query != null) {
    742                 ret = query.compareTo(uri.query);
    743                 if (ret != 0) {
    744                     return ret;
    745                 }
    746             }
    747         }
    748 
    749         // everything else is identical, so compare fragments
    750         if (fragment != null && uri.fragment == null) {
    751             return 1;
    752         } else if (fragment == null && uri.fragment != null) {
    753             return -1;
    754         } else if (fragment != null && uri.fragment != null) {
    755             ret = fragment.compareTo(uri.fragment);
    756             if (ret != 0) {
    757                 return ret;
    758             }
    759         }
    760 
    761         // identical
    762         return 0;
    763     }
    764 
    765     /**
    766      * Returns the URI formed by parsing {@code uri}. This method behaves
    767      * identically to the string constructor but throws a different exception
    768      * on failure. The constructor fails with a checked {@link
    769      * URISyntaxException}; this method fails with an unchecked {@link
    770      * IllegalArgumentException}.
    771      */
    772     public static URI create(String uri) {
    773         try {
    774             return new URI(uri);
    775         } catch (URISyntaxException e) {
    776             throw new IllegalArgumentException(e.getMessage());
    777         }
    778     }
    779 
    780     private URI duplicate() {
    781         URI clone = new URI();
    782         clone.absolute = absolute;
    783         clone.authority = authority;
    784         clone.fragment = fragment;
    785         clone.host = host;
    786         clone.opaque = opaque;
    787         clone.path = path;
    788         clone.port = port;
    789         clone.query = query;
    790         clone.scheme = scheme;
    791         clone.schemeSpecificPart = schemeSpecificPart;
    792         clone.userInfo = userInfo;
    793         clone.serverAuthority = serverAuthority;
    794         return clone;
    795     }
    796 
    797     /*
    798      * Takes a string that may contain hex sequences like %F1 or %2b and
    799      * converts the hex values following the '%' to lowercase
    800      */
    801     private String convertHexToLowerCase(String s) {
    802         StringBuilder result = new StringBuilder("");
    803         if (s.indexOf('%') == -1) {
    804             return s;
    805         }
    806 
    807         int index, prevIndex = 0;
    808         while ((index = s.indexOf('%', prevIndex)) != -1) {
    809             result.append(s.substring(prevIndex, index + 1));
    810             result.append(s.substring(index + 1, index + 3).toLowerCase());
    811             index += 3;
    812             prevIndex = index;
    813         }
    814         return result.toString();
    815     }
    816 
    817     /**
    818      * Returns true if {@code first} and {@code second} are equal after
    819      * unescaping hex sequences like %F1 and %2b.
    820      */
    821     private boolean escapedEquals(String first, String second) {
    822         if (first.indexOf('%') != second.indexOf('%')) {
    823             return first.equals(second);
    824         }
    825 
    826         int index, prevIndex = 0;
    827         while ((index = first.indexOf('%', prevIndex)) != -1
    828                 && second.indexOf('%', prevIndex) == index) {
    829             boolean match = first.substring(prevIndex, index).equals(
    830                     second.substring(prevIndex, index));
    831             if (!match) {
    832                 return false;
    833             }
    834 
    835             match = first.substring(index + 1, index + 3).equalsIgnoreCase(
    836                     second.substring(index + 1, index + 3));
    837             if (!match) {
    838                 return false;
    839             }
    840 
    841             index += 3;
    842             prevIndex = index;
    843         }
    844         return first.substring(prevIndex).equals(second.substring(prevIndex));
    845     }
    846 
    847     /**
    848      * Compares this URI instance with the given argument {@code o} and
    849      * determines if both are equal. Two URI instances are equal if all single
    850      * parts are identical in their meaning.
    851      *
    852      * @param o
    853      *            the URI this instance has to be compared with.
    854      * @return {@code true} if both URI instances point to the same resource,
    855      *         {@code false} otherwise.
    856      */
    857     @Override
    858     public boolean equals(Object o) {
    859         if (!(o instanceof URI)) {
    860             return false;
    861         }
    862         URI uri = (URI) o;
    863 
    864         if (uri.fragment == null && fragment != null || uri.fragment != null
    865                 && fragment == null) {
    866             return false;
    867         } else if (uri.fragment != null && fragment != null) {
    868             if (!escapedEquals(uri.fragment, fragment)) {
    869                 return false;
    870             }
    871         }
    872 
    873         if (uri.scheme == null && scheme != null || uri.scheme != null
    874                 && scheme == null) {
    875             return false;
    876         } else if (uri.scheme != null && scheme != null) {
    877             if (!uri.scheme.equalsIgnoreCase(scheme)) {
    878                 return false;
    879             }
    880         }
    881 
    882         if (uri.opaque && opaque) {
    883             return escapedEquals(uri.schemeSpecificPart,
    884                     schemeSpecificPart);
    885         } else if (!uri.opaque && !opaque) {
    886             if (!escapedEquals(path, uri.path)) {
    887                 return false;
    888             }
    889 
    890             if (uri.query != null && query == null || uri.query == null
    891                     && query != null) {
    892                 return false;
    893             } else if (uri.query != null && query != null) {
    894                 if (!escapedEquals(uri.query, query)) {
    895                     return false;
    896                 }
    897             }
    898 
    899             if (uri.authority != null && authority == null
    900                     || uri.authority == null && authority != null) {
    901                 return false;
    902             } else if (uri.authority != null && authority != null) {
    903                 if (uri.host != null && host == null || uri.host == null
    904                         && host != null) {
    905                     return false;
    906                 } else if (uri.host == null && host == null) {
    907                     // both are registry based, so compare the whole authority
    908                     return escapedEquals(uri.authority, authority);
    909                 } else { // uri.host != null && host != null, so server-based
    910                     if (!host.equalsIgnoreCase(uri.host)) {
    911                         return false;
    912                     }
    913 
    914                     if (port != uri.port) {
    915                         return false;
    916                     }
    917 
    918                     if (uri.userInfo != null && userInfo == null
    919                             || uri.userInfo == null && userInfo != null) {
    920                         return false;
    921                     } else if (uri.userInfo != null && userInfo != null) {
    922                         return escapedEquals(userInfo, uri.userInfo);
    923                     } else {
    924                         return true;
    925                     }
    926                 }
    927             } else {
    928                 // no authority
    929                 return true;
    930             }
    931 
    932         } else {
    933             // one is opaque, the other hierarchical
    934             return false;
    935         }
    936     }
    937 
    938     /**
    939      * Gets the decoded authority part of this URI.
    940      *
    941      * @return the decoded authority part or {@code null} if undefined.
    942      */
    943     public String getAuthority() {
    944         return decode(authority);
    945     }
    946 
    947     /**
    948      * Gets the decoded fragment part of this URI.
    949      *
    950      * @return the decoded fragment part or {@code null} if undefined.
    951      */
    952     public String getFragment() {
    953         return decode(fragment);
    954     }
    955 
    956     /**
    957      * Gets the host part of this URI.
    958      *
    959      * @return the host part or {@code null} if undefined.
    960      */
    961     public String getHost() {
    962         return host;
    963     }
    964 
    965     /**
    966      * Gets the decoded path part of this URI.
    967      *
    968      * @return the decoded path part or {@code null} if undefined.
    969      */
    970     public String getPath() {
    971         return decode(path);
    972     }
    973 
    974     /**
    975      * Gets the port number of this URI.
    976      *
    977      * @return the port number or {@code -1} if undefined.
    978      */
    979     public int getPort() {
    980         return port;
    981     }
    982 
    983     /** @hide */
    984     public int getEffectivePort() {
    985         return getEffectivePort(scheme, port);
    986     }
    987 
    988     /**
    989      * Returns the port to use for {@code scheme} connections will use when
    990      * {@link #getPort} returns {@code specifiedPort}.
    991      *
    992      * @hide
    993      */
    994     public static int getEffectivePort(String scheme, int specifiedPort) {
    995         if (specifiedPort != -1) {
    996             return specifiedPort;
    997         }
    998 
    999         if ("http".equalsIgnoreCase(scheme)) {
   1000             return 80;
   1001         } else if ("https".equalsIgnoreCase(scheme)) {
   1002             return 443;
   1003         } else {
   1004             return -1;
   1005         }
   1006     }
   1007 
   1008     /**
   1009      * Gets the decoded query part of this URI.
   1010      *
   1011      * @return the decoded query part or {@code null} if undefined.
   1012      */
   1013     public String getQuery() {
   1014         return decode(query);
   1015     }
   1016 
   1017     /**
   1018      * Gets the authority part of this URI in raw form.
   1019      *
   1020      * @return the encoded authority part or {@code null} if undefined.
   1021      */
   1022     public String getRawAuthority() {
   1023         return authority;
   1024     }
   1025 
   1026     /**
   1027      * Gets the fragment part of this URI in raw form.
   1028      *
   1029      * @return the encoded fragment part or {@code null} if undefined.
   1030      */
   1031     public String getRawFragment() {
   1032         return fragment;
   1033     }
   1034 
   1035     /**
   1036      * Gets the path part of this URI in raw form.
   1037      *
   1038      * @return the encoded path part or {@code null} if undefined.
   1039      */
   1040     public String getRawPath() {
   1041         return path;
   1042     }
   1043 
   1044     /**
   1045      * Gets the query part of this URI in raw form.
   1046      *
   1047      * @return the encoded query part or {@code null} if undefined.
   1048      */
   1049     public String getRawQuery() {
   1050         return query;
   1051     }
   1052 
   1053     /**
   1054      * Gets the scheme-specific part of this URI in raw form.
   1055      *
   1056      * @return the encoded scheme-specific part or {@code null} if undefined.
   1057      */
   1058     public String getRawSchemeSpecificPart() {
   1059         return schemeSpecificPart;
   1060     }
   1061 
   1062     /**
   1063      * Gets the user-info part of this URI in raw form.
   1064      *
   1065      * @return the encoded user-info part or {@code null} if undefined.
   1066      */
   1067     public String getRawUserInfo() {
   1068         return userInfo;
   1069     }
   1070 
   1071     /**
   1072      * Gets the scheme part of this URI.
   1073      *
   1074      * @return the scheme part or {@code null} if undefined.
   1075      */
   1076     public String getScheme() {
   1077         return scheme;
   1078     }
   1079 
   1080     /**
   1081      * Gets the decoded scheme-specific part of this URI.
   1082      *
   1083      * @return the decoded scheme-specific part or {@code null} if undefined.
   1084      */
   1085     public String getSchemeSpecificPart() {
   1086         return decode(schemeSpecificPart);
   1087     }
   1088 
   1089     /**
   1090      * Gets the decoded user-info part of this URI.
   1091      *
   1092      * @return the decoded user-info part or {@code null} if undefined.
   1093      */
   1094     public String getUserInfo() {
   1095         return decode(userInfo);
   1096     }
   1097 
   1098     /**
   1099      * Gets the hashcode value of this URI instance.
   1100      *
   1101      * @return the appropriate hashcode value.
   1102      */
   1103     @Override
   1104     public int hashCode() {
   1105         if (hash == -1) {
   1106             hash = getHashString().hashCode();
   1107         }
   1108         return hash;
   1109     }
   1110 
   1111     /**
   1112      * Indicates whether this URI is absolute, which means that a scheme part is
   1113      * defined in this URI.
   1114      *
   1115      * @return {@code true} if this URI is absolute, {@code false} otherwise.
   1116      */
   1117     public boolean isAbsolute() {
   1118         return absolute;
   1119     }
   1120 
   1121     /**
   1122      * Indicates whether this URI is opaque or not. An opaque URI is absolute
   1123      * and has a scheme-specific part which does not start with a slash
   1124      * character. All parts except scheme, scheme-specific and fragment are
   1125      * undefined.
   1126      *
   1127      * @return {@code true} if the URI is opaque, {@code false} otherwise.
   1128      */
   1129     public boolean isOpaque() {
   1130         return opaque;
   1131     }
   1132 
   1133     /*
   1134      * normalize path, and return the resulting string
   1135      */
   1136     private String normalize(String path) {
   1137         // count the number of '/'s, to determine number of segments
   1138         int index = -1;
   1139         int pathLength = path.length();
   1140         int size = 0;
   1141         if (pathLength > 0 && path.charAt(0) != '/') {
   1142             size++;
   1143         }
   1144         while ((index = path.indexOf('/', index + 1)) != -1) {
   1145             if (index + 1 < pathLength && path.charAt(index + 1) != '/') {
   1146                 size++;
   1147             }
   1148         }
   1149 
   1150         String[] segList = new String[size];
   1151         boolean[] include = new boolean[size];
   1152 
   1153         // break the path into segments and store in the list
   1154         int current = 0;
   1155         int index2;
   1156         index = (pathLength > 0 && path.charAt(0) == '/') ? 1 : 0;
   1157         while ((index2 = path.indexOf('/', index + 1)) != -1) {
   1158             segList[current++] = path.substring(index, index2);
   1159             index = index2 + 1;
   1160         }
   1161 
   1162         // if current==size, then the last character was a slash
   1163         // and there are no more segments
   1164         if (current < size) {
   1165             segList[current] = path.substring(index);
   1166         }
   1167 
   1168         // determine which segments get included in the normalized path
   1169         for (int i = 0; i < size; i++) {
   1170             include[i] = true;
   1171             if (segList[i].equals("..")) {
   1172                 int remove = i - 1;
   1173                 // search back to find a segment to remove, if possible
   1174                 while (remove > -1 && !include[remove]) {
   1175                     remove--;
   1176                 }
   1177                 // if we find a segment to remove, remove it and the ".."
   1178                 // segment
   1179                 if (remove > -1 && !segList[remove].equals("..")) {
   1180                     include[remove] = false;
   1181                     include[i] = false;
   1182                 }
   1183             } else if (segList[i].equals(".")) {
   1184                 include[i] = false;
   1185             }
   1186         }
   1187 
   1188         // put the path back together
   1189         StringBuilder newPath = new StringBuilder();
   1190         if (path.startsWith("/")) {
   1191             newPath.append('/');
   1192         }
   1193 
   1194         for (int i = 0; i < segList.length; i++) {
   1195             if (include[i]) {
   1196                 newPath.append(segList[i]);
   1197                 newPath.append('/');
   1198             }
   1199         }
   1200 
   1201         // if we used at least one segment and the path previously ended with
   1202         // a slash and the last segment is still used, then delete the extra
   1203         // trailing '/'
   1204         if (!path.endsWith("/") && segList.length > 0
   1205                 && include[segList.length - 1]) {
   1206             newPath.deleteCharAt(newPath.length() - 1);
   1207         }
   1208 
   1209         String result = newPath.toString();
   1210 
   1211         // check for a ':' in the first segment if one exists,
   1212         // prepend "./" to normalize
   1213         index = result.indexOf(':');
   1214         index2 = result.indexOf('/');
   1215         if (index != -1 && (index < index2 || index2 == -1)) {
   1216             newPath.insert(0, "./");
   1217             result = newPath.toString();
   1218         }
   1219         return result;
   1220     }
   1221 
   1222     /**
   1223      * Normalizes the path part of this URI.
   1224      *
   1225      * @return an URI object which represents this instance with a normalized
   1226      *         path.
   1227      */
   1228     public URI normalize() {
   1229         if (opaque) {
   1230             return this;
   1231         }
   1232         String normalizedPath = normalize(path);
   1233         // if the path is already normalized, return this
   1234         if (path.equals(normalizedPath)) {
   1235             return this;
   1236         }
   1237         // get an exact copy of the URI re-calculate the scheme specific part
   1238         // since the path of the normalized URI is different from this URI.
   1239         URI result = duplicate();
   1240         result.path = normalizedPath;
   1241         result.setSchemeSpecificPart();
   1242         return result;
   1243     }
   1244 
   1245     /**
   1246      * Tries to parse the authority component of this URI to divide it into the
   1247      * host, port, and user-info. If this URI is already determined as a
   1248      * ServerAuthority this instance will be returned without changes.
   1249      *
   1250      * @return this instance with the components of the parsed server authority.
   1251      * @throws URISyntaxException
   1252      *             if the authority part could not be parsed as a server-based
   1253      *             authority.
   1254      */
   1255     public URI parseServerAuthority() throws URISyntaxException {
   1256         if (!serverAuthority) {
   1257             parseAuthority(true);
   1258         }
   1259         return this;
   1260     }
   1261 
   1262     /**
   1263      * Makes the given URI {@code relative} to a relative URI against the URI
   1264      * represented by this instance.
   1265      *
   1266      * @param relative
   1267      *            the URI which has to be relativized against this URI.
   1268      * @return the relative URI.
   1269      */
   1270     public URI relativize(URI relative) {
   1271         if (relative.opaque || opaque) {
   1272             return relative;
   1273         }
   1274 
   1275         if (scheme == null ? relative.scheme != null : !scheme
   1276                 .equals(relative.scheme)) {
   1277             return relative;
   1278         }
   1279 
   1280         if (authority == null ? relative.authority != null : !authority
   1281                 .equals(relative.authority)) {
   1282             return relative;
   1283         }
   1284 
   1285         // normalize both paths
   1286         String thisPath = normalize(path);
   1287         String relativePath = normalize(relative.path);
   1288 
   1289         /*
   1290          * if the paths aren't equal, then we need to determine if this URI's
   1291          * path is a parent path (begins with) the relative URI's path
   1292          */
   1293         if (!thisPath.equals(relativePath)) {
   1294             // if this URI's path doesn't end in a '/', add one
   1295             if (!thisPath.endsWith("/")) {
   1296                 thisPath = thisPath + '/';
   1297             }
   1298             /*
   1299              * if the relative URI's path doesn't start with this URI's path,
   1300              * then just return the relative URI; the URIs have nothing in
   1301              * common
   1302              */
   1303             if (!relativePath.startsWith(thisPath)) {
   1304                 return relative;
   1305             }
   1306         }
   1307 
   1308         URI result = new URI();
   1309         result.fragment = relative.fragment;
   1310         result.query = relative.query;
   1311         // the result URI is the remainder of the relative URI's path
   1312         result.path = relativePath.substring(thisPath.length());
   1313         result.setSchemeSpecificPart();
   1314         return result;
   1315     }
   1316 
   1317     /**
   1318      * Resolves the given URI {@code relative} against the URI represented by
   1319      * this instance.
   1320      *
   1321      * @param relative
   1322      *            the URI which has to be resolved against this URI.
   1323      * @return the resolved URI.
   1324      */
   1325     public URI resolve(URI relative) {
   1326         if (relative.absolute || opaque) {
   1327             return relative;
   1328         }
   1329 
   1330         URI result;
   1331         if (relative.path.isEmpty() && relative.scheme == null
   1332                 && relative.authority == null && relative.query == null
   1333                 && relative.fragment != null) {
   1334             // if the relative URI only consists of fragment,
   1335             // the resolved URI is very similar to this URI,
   1336             // except that it has the fragment from the relative URI.
   1337             result = duplicate();
   1338             result.fragment = relative.fragment;
   1339             // no need to re-calculate the scheme specific part,
   1340             // since fragment is not part of scheme specific part.
   1341             return result;
   1342         }
   1343 
   1344         if (relative.authority != null) {
   1345             // if the relative URI has authority,
   1346             // the resolved URI is almost the same as the relative URI,
   1347             // except that it has the scheme of this URI.
   1348             result = relative.duplicate();
   1349             result.scheme = scheme;
   1350             result.absolute = absolute;
   1351         } else {
   1352             // since relative URI has no authority,
   1353             // the resolved URI is very similar to this URI,
   1354             // except that it has the query and fragment of the relative URI,
   1355             // and the path is different.
   1356             result = duplicate();
   1357             result.fragment = relative.fragment;
   1358             result.query = relative.query;
   1359             if (relative.path.startsWith("/")) {
   1360                 result.path = relative.path;
   1361             } else {
   1362                 // resolve a relative reference
   1363                 int endIndex = path.lastIndexOf('/') + 1;
   1364                 result.path = normalize(path.substring(0, endIndex)
   1365                         + relative.path);
   1366             }
   1367             // re-calculate the scheme specific part since
   1368             // query and path of the resolved URI is different from this URI.
   1369             result.setSchemeSpecificPart();
   1370         }
   1371         return result;
   1372     }
   1373 
   1374     /**
   1375      * Helper method used to re-calculate the scheme specific part of the
   1376      * resolved or normalized URIs
   1377      */
   1378     private void setSchemeSpecificPart() {
   1379         // ssp = [//authority][path][?query]
   1380         StringBuilder ssp = new StringBuilder();
   1381         if (authority != null) {
   1382             ssp.append("//" + authority);
   1383         }
   1384         if (path != null) {
   1385             ssp.append(path);
   1386         }
   1387         if (query != null) {
   1388             ssp.append("?" + query);
   1389         }
   1390         schemeSpecificPart = ssp.toString();
   1391         // reset string, so that it can be re-calculated correctly when asked.
   1392         string = null;
   1393     }
   1394 
   1395     /**
   1396      * Creates a new URI instance by parsing the given string {@code relative}
   1397      * and resolves the created URI against the URI represented by this
   1398      * instance.
   1399      *
   1400      * @param relative
   1401      *            the given string to create the new URI instance which has to
   1402      *            be resolved later on.
   1403      * @return the created and resolved URI.
   1404      */
   1405     public URI resolve(String relative) {
   1406         return resolve(create(relative));
   1407     }
   1408 
   1409     /**
   1410      * Encode unicode chars that are not part of US-ASCII char set into the
   1411      * escaped form
   1412      *
   1413      * i.e. The Euro currency symbol is encoded as "%E2%82%AC".
   1414      */
   1415     private String encodeNonAscii(String s) {
   1416         try {
   1417             /*
   1418              * Use a different encoder than URLEncoder since: 1. chars like "/",
   1419              * "#", "@" etc needs to be preserved instead of being encoded, 2.
   1420              * UTF-8 char set needs to be used for encoding instead of default
   1421              * platform one 3. Only other chars need to be converted
   1422              */
   1423             return URIEncoderDecoder.encodeOthers(s);
   1424         } catch (UnsupportedEncodingException e) {
   1425             throw new RuntimeException(e.toString());
   1426         }
   1427     }
   1428 
   1429     private String decode(String s) {
   1430         if (s == null) {
   1431             return s;
   1432         }
   1433 
   1434         try {
   1435             return URIEncoderDecoder.decode(s);
   1436         } catch (UnsupportedEncodingException e) {
   1437             throw new RuntimeException(e.toString());
   1438         }
   1439     }
   1440 
   1441     /**
   1442      * Returns the textual string representation of this URI instance using the
   1443      * US-ASCII encoding.
   1444      *
   1445      * @return the US-ASCII string representation of this URI.
   1446      */
   1447     public String toASCIIString() {
   1448         return encodeNonAscii(toString());
   1449     }
   1450 
   1451     /**
   1452      * Returns the textual string representation of this URI instance.
   1453      *
   1454      * @return the textual string representation of this URI.
   1455      */
   1456     @Override
   1457     public String toString() {
   1458         if (string == null) {
   1459             StringBuilder result = new StringBuilder();
   1460             if (scheme != null) {
   1461                 result.append(scheme);
   1462                 result.append(':');
   1463             }
   1464             if (opaque) {
   1465                 result.append(schemeSpecificPart);
   1466             } else {
   1467                 if (authority != null) {
   1468                     result.append("//");
   1469                     result.append(authority);
   1470                 }
   1471 
   1472                 if (path != null) {
   1473                     result.append(path);
   1474                 }
   1475 
   1476                 if (query != null) {
   1477                     result.append('?');
   1478                     result.append(query);
   1479                 }
   1480             }
   1481 
   1482             if (fragment != null) {
   1483                 result.append('#');
   1484                 result.append(fragment);
   1485             }
   1486 
   1487             string = result.toString();
   1488         }
   1489         return string;
   1490     }
   1491 
   1492     /*
   1493      * Form a string from the components of this URI, similarly to the
   1494      * toString() method. But this method converts scheme and host to lowercase,
   1495      * and converts escaped octets to lowercase.
   1496      */
   1497     private String getHashString() {
   1498         StringBuilder result = new StringBuilder();
   1499         if (scheme != null) {
   1500             result.append(scheme.toLowerCase());
   1501             result.append(':');
   1502         }
   1503         if (opaque) {
   1504             result.append(schemeSpecificPart);
   1505         } else {
   1506             if (authority != null) {
   1507                 result.append("//");
   1508                 if (host == null) {
   1509                     result.append(authority);
   1510                 } else {
   1511                     if (userInfo != null) {
   1512                         result.append(userInfo + "@");
   1513                     }
   1514                     result.append(host.toLowerCase());
   1515                     if (port != -1) {
   1516                         result.append(":" + port);
   1517                     }
   1518                 }
   1519             }
   1520 
   1521             if (path != null) {
   1522                 result.append(path);
   1523             }
   1524 
   1525             if (query != null) {
   1526                 result.append('?');
   1527                 result.append(query);
   1528             }
   1529         }
   1530 
   1531         if (fragment != null) {
   1532             result.append('#');
   1533             result.append(fragment);
   1534         }
   1535 
   1536         return convertHexToLowerCase(result.toString());
   1537     }
   1538 
   1539     /**
   1540      * Converts this URI instance to a URL.
   1541      *
   1542      * @return the created URL representing the same resource as this URI.
   1543      * @throws MalformedURLException
   1544      *             if an error occurs while creating the URL or no protocol
   1545      *             handler could be found.
   1546      */
   1547     public URL toURL() throws MalformedURLException {
   1548         if (!absolute) {
   1549             throw new IllegalArgumentException("URI is not absolute: " + toString());
   1550         }
   1551         return new URL(toString());
   1552     }
   1553 
   1554     private void readObject(ObjectInputStream in) throws IOException,
   1555             ClassNotFoundException {
   1556         in.defaultReadObject();
   1557         try {
   1558             parseURI(string, false);
   1559         } catch (URISyntaxException e) {
   1560             throw new IOException(e.toString());
   1561         }
   1562     }
   1563 
   1564     private void writeObject(ObjectOutputStream out) throws IOException,
   1565             ClassNotFoundException {
   1566         // call toString() to ensure the value of string field is calculated
   1567         toString();
   1568         out.defaultWriteObject();
   1569     }
   1570 }
   1571