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