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