001/**
002 * Java Web Archive Toolkit - Software to read and validate ARC, WARC
003 * and GZip files. (http://jwat.org/)
004 * Copyright 2011-2012 Netarkivet.dk (http://netarkivet.dk/)
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License");
007 * you may not use this file except in compliance with the License.
008 * You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.jwat.common;
019
020import java.net.URISyntaxException;
021
022/**
023 * Custom URI parser/validation based on rfc3986. An URI is split into sub
024 * components which are in turn validated. The overall reason for implementing
025 * this class is the lack of support for %uxxxx encoding which is not part of
026 * the specification but nevertheless needs to be validated since they are in
027 * use. Other non standard characters are also used in the wild.
028 *
029 * This implementation is not a complete substitute for the JDK version.
030 * It does not yet implement the following methods.
031 * - normalize()
032 * - parseServerAuthority()
033 * - relativize(URI uri)
034 * - resolve(String str)
035 * - resolve(URI uri)
036 * - toASCIIString()
037 *
038 * @author nicl
039 */
040public class Uri implements Comparable<Uri> {
041
042    /** Raw hier-part part ("//" + authority + path). */
043    protected String hierPartRaw;
044    /** Raw scheme specific part, if present. */
045    protected String schemeSpecificPartRaw;
046    /** Raw authority components, if present. */
047    protected String authorityRaw;
048    /** Raw userinfo authority component, if present. */
049    protected String userinfoRaw;
050    /** Host authority component, if present. */
051    protected String hostRaw;
052    /** Raw port authority component, if present. */
053    protected String portRaw;
054    /** Raw path component. */
055    protected String pathRaw;
056    /** Raw query component, if present. */
057    protected String queryRaw;
058    /** Raw fragment component, if present. */
059    protected String fragmentRaw;
060
061    /** Scheme component, if present. */
062    protected String scheme;
063    /** Scheme specific part decoded. */
064    protected String schemeSpecificPart;
065    /** Authority components decoded, if present. */
066    protected String authority;
067    /** Userinfo authority component decoded, if present. */
068    protected String userinfo;
069    /** Host authority component decoded, if present. */
070    protected String host;
071    /** Port authority component, if present. */
072    protected int port = -1;
073    /** Path component decoded. */
074    protected String path;
075    /** Query component decoded, if present. */
076    protected String query;
077    /** Fragment component decoded, if present. */
078    protected String fragment;
079
080    /** Boolean indicating whether this URI is absolute. */
081    protected boolean bAbsolute;
082
083    /** Boolean indicating whether this URI is opaque, @see #isOpaque() */
084    protected boolean bOpaque;
085
086    /** Profile used to parse URI. */
087    protected UriProfile uriProfile = UriProfile.RFC3986;
088
089    /**
090     * Constructor for unit-testing.
091     */
092    protected Uri() {
093    }
094
095    /**
096     * Creates a URI by parsing the given string using the default RFC3986
097     * profile.
098     * @param str The string to be parsed into a URI
099     * @return The new URI
100     * @throws IllegalArgumentException If the given string violates RFC 3986
101     */
102    public static Uri create(String str) throws IllegalArgumentException {
103        try {
104            return new Uri(str, UriProfile.RFC3986);
105        } catch (URISyntaxException e) {
106            throw new IllegalArgumentException("Invalid URI", e);
107        }
108    }
109
110    /**
111     * Creates a URI by parsing the given string using the requested profile.
112     * @param str The string to be parsed into a URI
113     * @param uriProfile the requested profile
114     * @return The new URI
115     * @throws IllegalArgumentException If the given string violates the profile
116     */
117    public static Uri create(String str, UriProfile uriProfile) throws IllegalArgumentException {
118        try {
119            return new Uri(str, uriProfile);
120        } catch (URISyntaxException e) {
121            throw new IllegalArgumentException("Invalid URI", e);
122        }
123    }
124
125    /**
126     * Constructs a URI by parsing the given string using the default RFC3986
127     * profile.
128     * @param str The string to be parsed into a URI
129     * @throws URISyntaxException If the given string violates RFC 3986
130     */
131    public Uri(String str) throws URISyntaxException {
132        this(str, UriProfile.RFC3986);
133    }
134
135    /**
136     * Constructs a URI by parsing the given string using the requested
137     * profile.
138     * @param str The string to be parsed into a URI
139     * @param uriProfile the requested profile
140     * @throws URISyntaxException If the given string violates the profile
141     */
142    public Uri(String str, UriProfile uriProfile) throws URISyntaxException {
143        this.uriProfile = uriProfile;
144        int idx = uriProfile.indexOf(UriProfile.B_GEN_DELIMS, str, 0);
145        if (idx != -1 && str.charAt(idx) == ':') {
146            // Try validating as an absolute URI.
147            scheme = str.substring(0, idx++);
148            validate_absoluteUri(str, idx);
149            bAbsolute = true;
150            if (schemeSpecificPart.length() > 0
151                    && !schemeSpecificPart.startsWith("/")) {
152                bOpaque = true;
153            }
154        } else {
155            if (uriProfile.bAllowRelativeUris) {
156                validate_relativeUri(str, 0);
157            } else {
158                throw new URISyntaxException(str, "Invalid URI - relative URIs not allowed");
159            }
160        }
161    }
162
163    /**
164     * Parse and validate an absolute URI string.
165     * @param uriStr URI string
166     * @param uIdx index in string after scheme colon character
167     * @throws URISyntaxException if the URI is invalid in some way
168     */
169    protected void validate_absoluteUri(String uriStr, int uIdx) throws URISyntaxException {
170        // Scheme validation.
171        if (scheme.length() > 0) {
172            uriProfile.validate_first_follow(scheme, UriProfile.B_SCHEME_FIRST, UriProfile.B_SCHEME_FOLLOW);
173        } else {
174            throw new URISyntaxException(scheme, "Empty URI scheme component");
175        }
176        validate_relativeUri(uriStr, uIdx);
177    }
178
179    /**
180     * Parse and validate a relative URI string.
181     * This method is also used by the absolute parser after a scheme has
182     * been identified.
183     * @param uriStr URI string
184     * @param uIdx index in string to start parsing from
185     * @throws URISyntaxException if the URI is invalid in some way
186     */
187    protected void validate_relativeUri(String uriStr, int uIdx) throws URISyntaxException {
188        // QueryRaw.
189        int qfIdx = uriStr.length();
190        int qIdx = uriStr.indexOf('?', uIdx);
191        int fIdx;
192        if (qIdx != -1) {
193            qfIdx = qIdx++;
194            fIdx = uriStr.indexOf('#', qIdx);
195            if (fIdx != -1) {
196                queryRaw = uriStr.substring(qIdx, fIdx);
197            } else {
198                queryRaw = uriStr.substring(qIdx);
199            }
200        } else {
201            fIdx = uriStr.indexOf('#', uIdx);
202        }
203        // FragmentRaw.
204        if (fIdx != -1) {
205            if (fIdx < qfIdx) {
206                qfIdx = fIdx;
207            }
208            ++fIdx;
209            fragmentRaw = uriStr.substring(fIdx);
210        }
211        // HierPartRaw / AuthorityRaw / PathRaw.
212        hierPartRaw = uriStr.substring(uIdx, qfIdx);
213        if (hierPartRaw.startsWith("//")) {
214            int pIdx = hierPartRaw.indexOf('/', 2);
215            if (pIdx != -1) {
216                authorityRaw = hierPartRaw.substring(2, pIdx);
217                pathRaw = hierPartRaw.substring(pIdx);
218            } else {
219                authorityRaw = hierPartRaw.substring(2);
220                pathRaw = "";
221            }
222        } else {
223            pathRaw = hierPartRaw;
224        }
225        // SchemeSpecificPartRaw.
226        if (queryRaw != null) {
227            schemeSpecificPartRaw = hierPartRaw + '?' + queryRaw;
228        } else {
229            schemeSpecificPartRaw = hierPartRaw;
230        }
231        // Authority
232        if (authorityRaw != null) {
233            // UserinfoRaw.
234            int aIdx = authorityRaw.indexOf('@');
235            if (aIdx != -1) {
236                userinfoRaw = authorityRaw.substring(0, aIdx++);
237            } else {
238                aIdx = 0;
239            }
240            // Host / PortRaw.
241            if (aIdx < authorityRaw.length() && authorityRaw.charAt(aIdx) == '[') {
242                // ipv6 or new address type.
243                int bIdx = authorityRaw.indexOf(']', aIdx);
244                if (bIdx != -1) {
245                    ++bIdx;
246                    hostRaw = authorityRaw.substring(aIdx, bIdx);
247                    host = hostRaw;
248                    if (bIdx < authorityRaw.length()) {
249                        if (authorityRaw.charAt(bIdx++) == ':') {
250                            portRaw = authorityRaw.substring(bIdx);
251                        } else {
252                            throw new URISyntaxException(authorityRaw, "Invalid URI authority/port component - expected a ':'");
253                        }
254                    }
255                } else {
256                    throw new URISyntaxException(authorityRaw, "Invalid URI authority/host component - missing ']'");
257                }
258            } else {
259                // ipv4 or hostname.
260                int pIdx = authorityRaw.indexOf(':', aIdx);
261                if (pIdx != -1) {
262                    hostRaw = authorityRaw.substring(aIdx, pIdx++);
263                    portRaw = authorityRaw.substring(pIdx);
264                } else {
265                    hostRaw = authorityRaw.substring(aIdx);
266                    //portRaw = null;
267                }
268                host = uriProfile.validate_decode(UriProfile.B_REGNAME, "host", hostRaw);
269            }
270            // Userinfo validation.
271            authority = "";
272            if (userinfoRaw != null) {
273                userinfo = uriProfile.validate_decode(UriProfile.B_USERINFO, "userinfo", userinfoRaw);
274                authority += userinfo + '@';
275            }
276            authority += host;
277            // Port validation.
278            if (portRaw != null) {
279                if (portRaw.length() > 0) {
280                    try {
281                        port = Integer.parseInt(portRaw);
282                        if (port < 1 || port > 65535) {
283                            throw new URISyntaxException(portRaw, "Invalid URI port component - port is not within range [1-65535]");
284                        }
285                        authority += ':' + portRaw;
286                    } catch (NumberFormatException e) {
287                        throw new URISyntaxException(portRaw, "Invalid URI port component");
288                    }
289                } else {
290                    authority += ':';
291                }
292            }
293            schemeSpecificPart = "//" + authority;
294            // Path validation (path-abempty).
295            if (pathRaw.length() > 0) {
296                path = uriProfile.validate_decode(UriProfile.B_PATH, "path", pathRaw);
297            } else {
298                path = "";
299            }
300            schemeSpecificPart += path;
301        } else {
302            // Path validation (path-absolute / path-rootless / path-empty).
303            if (pathRaw.length() > 0) {
304                path = uriProfile.validate_decode(UriProfile.B_PATH, "path", pathRaw);
305            } else {
306                path = "";
307            }
308            schemeSpecificPart = path;
309        }
310        // Query validation.
311        if (queryRaw != null) {
312            query = uriProfile.validate_decode(UriProfile.B_QUERY, "query", queryRaw);
313            schemeSpecificPart += '?' + query;
314        }
315        // Fragment validation.
316        if (fragmentRaw != null) {
317            fragment = uriProfile.validate_decode(UriProfile.B_FRAGMENT, "fragment", fragmentRaw);
318        }
319    }
320
321    /**
322     * Tells whether or not this URI is absolute.
323     * A URI is absolute if, and only if, it has a scheme component.
324     * @return true if, and only if, this URI is absolute
325     */
326    public boolean isAbsolute() {
327        return bAbsolute;
328    }
329
330    /**
331     * Tells whether or not this URI is opaque.
332     * A URI is opaque if, and only if, it is absolute and its scheme-specific
333     * part does not begin with a slash character ('/'). An opaque URI has a
334     * scheme, a scheme-specific part, and possibly a fragment; all other
335     * components are undefined.
336     * @return true if, and only if, this URI is opaque
337     */
338    public boolean isOpaque() {
339        return bOpaque;
340    }
341
342    /**
343     * Returns the scheme component of this URI.
344     * The scheme component of a URI, if defined, only contains characters in
345     * the alphanum category and in the string "-.+". A scheme always starts
346     * with an alpha character.
347     * The scheme component of a URI cannot contain escaped octets, hence this
348     * method does not perform any decoding.
349     * @return The scheme component of this URI, or null if the scheme is
350     * undefined
351     */
352    public String getScheme() {
353        return scheme;
354    }
355
356    /**
357     * Returns the raw scheme-specific part of this URI.
358     * The scheme-specific part is never undefined, though it may be empty.
359     * The scheme-specific part of a URI only contains legal URI characters.
360     * @return The raw scheme-specific part of this URI (never null)
361     */
362    public String getRawSchemeSpecificPart() {
363        return schemeSpecificPartRaw;
364    }
365
366    /**
367     * Returns the decoded scheme-specific part of this URI.
368     * The string returned by this method is equal to that returned by the
369     * getRawSchemeSpecificPart method except that all sequences of escaped
370     * octets are decoded.
371     * @return The decoded scheme-specific part of this URI (never null)
372     */
373    public String getSchemeSpecificPart() {
374        return schemeSpecificPart;
375    }
376
377    /**
378     * Returns the raw authority component of this URI.
379     * The authority component of a URI, if defined, only contains the
380     * commercial-at character ('@') and characters in the unreserved, punct,
381     * escaped, and other categories. If the authority is server-based then it
382     * is further constrained to have valid user-information, host, and port
383     * components.
384     * @return The raw authority component of this URI, or null if the authority
385     * is undefined
386     */
387    public String getRawAuthority() {
388        return authorityRaw;
389    }
390
391    /**
392     * Returns the decoded authority component of this URI.
393     * The string returned by this method is equal to that returned by the
394     * getRawAuthority method except that all sequences of escaped octets are
395     * decoded.
396     * @return The decoded authority component of this URI, or null if the
397     * authority is undefined
398     */
399    public String getAuthority() {
400        return authority;
401    }
402
403    /**
404     * Returns the raw user-information component of this URI.
405     * The user-information component of a URI, if defined, only contains
406     * characters in the unreserved, punct, escaped, and other categories.
407     * @return The raw user-information component of this URI, or null if the
408     * user information is undefined
409     */
410    public String getRawUserInfo() {
411        return userinfoRaw;
412    }
413
414    /**
415     * Returns the decoded user-information component of this URI.
416     * The string returned by this method is equal to that returned by the
417     * getRawUserInfo method except that all sequences of escaped octets are
418     * decoded.
419     * @return The decoded user-information component of this URI, or null if
420     * the user information is undefined
421     */
422    public String getUserInfo() {
423        return userinfo;
424    }
425
426    /**
427     * Returns the host component of this URI.
428     * The host component of a URI, if defined, will have one of the following forms:
429     * - A domain name consisting of one or more labels separated by period
430     *   characters ('.'), optionally followed by a period character.
431     *   Each label consists of alphanum characters as well as hyphen characters ('-'),
432     *   though hyphens never occur as the first or last characters in a label.
433     *   The rightmost label of a domain name consisting of two or more labels,
434     *    begins with an alpha character.
435     * - A dotted-quad IPv4 address of the form digit+.digit+.digit+.digit+,
436     *   where no digit sequence is longer than three characters and no sequence
437     *   has a value larger than 255.
438     * - An IPv6 address enclosed in square brackets ('[' and ']') and consisting
439     *   of hexadecimal digits, colon characters (':'), and possibly an embedded
440     *   IPv4 address. The full syntax of IPv6 addresses is specified in
441     *   RFC 2373: IPv6 Addressing Architecture.
442     * The host component of a URI cannot contain escaped octets,
443     * hence this method does not perform any decoding.
444     * @return The host component of this URI, or null if the host is undefined
445     */
446    public String getHost() {
447        return host;
448    }
449
450    /**
451     * Returns the port number of this URI. The port component of a URI,
452     * if defined, is a non-negative integer between 1 and 65535.
453     * @return The port component of this URI, or -1 if the port is undefined
454     */
455    public int getPort() {
456        return port;
457    }
458
459    /**
460     * Returns the raw path component of this URI.
461     * The path component of a URI, if defined, only contains the slash
462     * character ('/'), the commercial-at character ('@'), and characters in
463     * the unreserved, punct, escaped, and other categories.
464     * @return The path component of this URI, or null if the path is undefined
465     */
466    public String getRawPath() {
467        return pathRaw;
468    }
469
470    /**
471     * Returns the decoded path component of this URI.
472     * The string returned by this method is equal to that returned by the
473     * getRawPath method except that all sequences of escaped octets are
474     * decoded.
475     * @return The decoded path component of this URI, or null if the path is
476     * undefined
477     */
478    public String getPath() {
479        return path;
480    }
481
482    /**
483     * Returns the raw query component of this URI.
484     * The query component of a URI, if defined, only contains legal URI
485     * characters.
486     * @return The raw query component of this URI, or null if the query is
487     * undefined
488     */
489    public String getRawQuery() {
490        return queryRaw;
491    }
492
493    /**
494     * Returns the decoded query component of this URI.
495     * The string returned by this method is equal to that returned by the
496     * getRawQuery method except that all sequences of escaped octets are
497     * decoded.
498     * @return The decoded query component of this URI, or null if the query is
499     * undefined
500     */
501    public String getQuery() {
502        return query;
503    }
504
505    /**
506     * Returns the raw fragment component of this URI.
507     * The fragment component of a URI, if defined, only contains legal URI
508     * characters.
509     * @return The raw fragment component of this URI, or null if the fragment
510     * is undefined
511     */
512    public String getRawFragment() {
513        return fragmentRaw;
514    }
515
516    /**
517     * Returns the decoded fragment component of this URI.
518     * The string returned by this method is equal to that returned by the
519     * getRawFragment method except that all sequences of escaped octets are
520     * decoded.
521     * @return The decoded fragment component of this URI, or null if the
522     * fragment is undefined
523     */
524    public String getFragment() {
525        return fragment;
526    }
527
528    @Override
529    public String toString() {
530        StringBuilder sb = new StringBuilder();
531        if (scheme != null) {
532            sb.append(scheme);
533            sb.append(':');
534        }
535        if (hostRaw != null) {
536            sb.append("//");
537            if (userinfoRaw != null) {
538                sb.append(userinfoRaw);
539                sb.append('@');
540            }
541            sb.append(hostRaw);
542            if (portRaw != null) {
543                sb.append(':');
544                sb.append(portRaw);
545            }
546        }
547        sb.append(pathRaw);
548        if (queryRaw != null) {
549            sb.append('?');
550            sb.append(queryRaw);
551        }
552        if (fragmentRaw != null) {
553            sb.append('#');
554            sb.append(fragmentRaw);
555        }
556        return sb.toString();
557    }
558
559    @Override
560    public boolean equals(Object obj) {
561        if (obj == null || !(obj instanceof Uri)) {
562            return false;
563        }
564        Uri uriObj = (Uri)obj;
565        if (scheme != null) {
566            if (!scheme.equals(uriObj.scheme)) {
567                return false;
568            }
569        } else if (uriObj.scheme != null) {
570            return false;
571        }
572        if (userinfo != null) {
573            if (!userinfo.equals(uriObj.userinfo)) {
574                return false;
575            }
576        } else if (uriObj.userinfo != null) {
577            return false;
578        }
579        if (host != null) {
580            if (!host.equals(uriObj.host)) {
581                return false;
582            }
583        } else if (uriObj.host != null) {
584            return false;
585        }
586        if (port != uriObj.port) {
587            return false;
588        }
589        if (!path.equals(uriObj.path)) {
590            return false;
591        }
592        if (query != null) {
593            if (!query.equals(uriObj.query)) {
594                return false;
595            }
596        } else if (uriObj.query != null) {
597            return false;
598        }
599        if (fragment != null) {
600            if (!fragment.equals(uriObj.fragment)) {
601                return false;
602            }
603        } else if (uriObj.fragment != null) {
604            return false;
605        }
606        return true;
607    }
608
609    @Override
610    public int hashCode() {
611        int hashCode = 0;
612        if (scheme != null) {
613            hashCode ^= scheme.hashCode();
614        }
615        hashCode ^= schemeSpecificPart.hashCode();
616        if (fragment != null) {
617            hashCode ^= fragment.hashCode();
618        }
619        return hashCode;
620    }
621
622    @Override
623    public int compareTo(Uri uri) {
624        if (uri == null) {
625            return 1;
626        }
627        int res = 0;
628        if (scheme != null) {
629            if (uri.scheme != null) {
630                res = scheme.compareTo(uri.scheme);
631                if (res != 0) {
632                    return res;
633                }
634            } else {
635                return 1;
636            }
637        } else if (uri.scheme != null) {
638            return -1;
639        }
640        res = schemeSpecificPart.compareTo(uri.schemeSpecificPart);
641        if (res != 0) {
642            return res;
643        }
644        if (fragment != null) {
645            if (uri.fragment != null) {
646                res = fragment.compareTo(uri.fragment);
647                if (res != 0) {
648                    return res;
649                }
650            } else {
651                return 1;
652            }
653        } else if (uri.fragment != null) {
654            return -1;
655        }
656        return res;
657    }
658
659}