1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.apache.river.api.net;
19
20 import java.io.File;
21 import java.io.UnsupportedEncodingException;
22 import java.net.MalformedURLException;
23 import java.net.URI;
24 import java.net.URISyntaxException;
25 import java.net.URL;
26 import org.apache.river.impl.Messages;
27
28
29 /**
30 * This class represents an immutable instance of a URI as defined by RFC 3986.
31 *
32 * This class replaces java.net.URI functionality.
33 *
34 * Unlike java.net.URI this class is not Serializable and hashCode and
35 * equality is governed by strict RFC3986 normalisation. In addition "other"
36 * characters allowed in java.net.URI as specified by javadoc, not specifically
37 * allowed by RFC3986 are illegal and must be escaped. This strict adherence
38 * is essential to eliminate false negative or positive matches.
39 *
40 * In addition to RFC3896 normalisation, on OS platforms with a \ file separator
41 * the path is converted to UPPER CASE for comparison for file: schema, during
42 * equals and hashCode calls.
43 *
44 * IPv6 and IPvFuture host addresses must be enclosed in square brackets as per
45 * RFC3986.
46 * @since 3.0.0
47 */
48 public final class Uri implements Comparable<Uri> {
49
50 /* Class Implementation */
51
52 /* Legacy java.net.URI RFC 2396 syntax*/
53 static final String unreserved = "_-!.~\'()*"; //$NON-NLS-1$
54 static final String punct = ",;:$&+="; //$NON-NLS-1$
55 static final String reserved = punct + "?/[]@"; //$NON-NLS-1$
56 // String someLegal = unreserved + punct;
57 // String queryLegal = unreserved + reserved + "\\\""; //$NON-NLS-1$
58 // String allLegal = unreserved + reserved;
59
60 static final String someLegal = unreserved + punct;
61
62 static final String queryLegal = unreserved + reserved + "\\\""; //$NON-NLS-1$
63
64 // static final String allLegal = unreserved + reserved;
65
66 /* RFC 3986 */
67 // private static final char [] latin = new char[256];
68 // private static final String [] latinEsc = new String[256];
69
70 /* 2.1. Percent-Encoding
71 *
72 * A percent-encoding mechanism is used to represent a data octet in a
73 * component when that octet's corresponding character is outside the
74 * allowed set or is being used as a delimiter of, or within, the
75 * component. A percent-encoded octet is encoded as a character
76 * triplet, consisting of the percent character "%" followed by the two
77 * hexadecimal digits representing that octet's numeric value. For
78 * example, "%20" is the percent-encoding for the binary octet
79 * "00100000" (ABNF: %x20), which in US-ASCII corresponds to the space
80 * character (SP). Section 2.4 describes when percent-encoding and
81 * decoding is applied.
82 *
83 * pct-encoded = "%" HEXDIG HEXDIG
84 *
85 * The uppercase hexadecimal digits 'A' through 'F' are equivalent to
86 * the lowercase digits 'a' through 'f', respectively. If two URIs
87 * differ only in the case of hexadecimal digits used in percent-encoded
88 * octets, they are equivalent. For consistency, URI producers and
89 * normalizers should use uppercase hexadecimal digits for all percent-
90 * encodings.
91 */
92 // Any character that is not part of the reserved and unreserved sets must
93 // be encoded.
94 // Section 2.1 Percent encoding must be converted to upper case during normalisation.
95 private static final char escape = '%';
96 /* RFC3986 obsoletes RFC2396 and RFC2732
97 *
98 * reserved = gen-delims / sub-delims
99 *
100 * gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
101 *
102 * sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
103 * / "*" / "+" / "," / ";" / "="
104 */
105 // Section 2.2 Reserved set is protected from normalisation.
106 // private static final char [] gen_delims = {':', '/', '?', '#', '[', ']', '@'};
107 // private static final char [] sub_delims = {'!', '$', '&', '\'', '(', ')', '*',
108 // '+', ',', ';', '='};
109 /*
110 * For consistency, percent-encoded octets in the ranges of ALPHA
111 * (%41-%5A and %61-%7A), DIGIT (%30-%39), hyphen (%2D), period (%2E),
112 * underscore (%5F), or tilde (%7E) should not be created by URI
113 * producers and, when found in a URI, should be decoded to their
114 * corresponding unreserved characters by URI normalizers.
115 */
116 // Section 2.3 Unreserved characters (Allowed) must be decoded during normalisation if % encoded.
117 // private static final char [] lowalpha = "abcdefghijklmnopqrstuvwxyz".toCharArray();
118 // private static final char [] upalpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
119 // private static final char [] numeric = "0123456789".toCharArray();
120 // private static final char [] unres_punct = {'-' , '.' , '_' , '~'};
121
122 // Section 3.1 Scheme
123 // private static final char [] schemeEx = "+-.".toCharArray(); // + ALPHA and numeric.
124
125 // To be unescaped during normalisation, unmodifiable and safely published.
126 // final static Map<String, Character> unReserved;
127 // final static Map<String, Character> schemeUnreserved;
128
129 /* Explicit legal String fields follow, ALPHA and DIGIT are implicitly legal */
130
131 /* All characters that are legal URI syntax */
132 static final String allLegalUnescaped = ":/?#[]@!$&'()*+,;=-._~";
133 static final String allLegal = "%:/?#[]@!$&'()*+,;=-._~";
134 /*
135 * Syntax Summary
136 *
137 * URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
138 *
139 * hier-part = "//" authority path-abempty
140 * / path-absolute
141 * / path-rootless
142 * / path-empty
143 *
144 * scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
145 */
146 static final String schemeLegal = "+-.";
147 /*
148 * authority = [ userinfo "@" ] host [ ":" port ]
149 * userinfo = *( unreserved / pct-encoded / sub-delims / ":" )
150 */
151 static final String userinfoLegal = "-._~!$&'()*+,;=:";
152 static final String authorityLegal = userinfoLegal + "@[]";
153 /* host = IP-literal / IPv4address / reg-name
154 * IP-literal = "[" ( IPv6address / IPvFuture ) "]"
155 * IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )
156 */
157 static final String iPvFuture = "-._~!$&'()*+,;=:";
158 /* IPv6address = 6( h16 ":" ) ls32
159 * / "::" 5( h16 ":" ) ls32
160 * / [ h16 ] "::" 4( h16 ":" ) ls32
161 * / [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32
162 * / [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32
163 * / [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32
164 * / [ *4( h16 ":" ) h16 ] "::" ls32
165 * / [ *5( h16 ":" ) h16 ] "::" h16
166 * / [ *6( h16 ":" ) h16 ] "::"
167 *
168 * ls32 = ( h16 ":" h16 ) / IPv4address
169 * ; least-significant 32 bits of address
170 *
171 * h16 = 1*4HEXDIG
172 * ; 16 bits of address represented in hexadecimal
173 *
174 * IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet
175 *
176 * dec-octet = DIGIT ; 0-9
177 * / %x31-39 DIGIT ; 10-99
178 * / "1" 2DIGIT ; 100-199
179 * / "2" %x30-34 DIGIT ; 200-249
180 * / "25" %x30-35 ; 250-255
181 * reg-name = *( unreserved / pct-encoded / sub-delims )
182 */
183 static final String hostRegNameLegal = "-._~!$&'()*+,;=";
184 /* port = *DIGIT
185 *
186 * path = path-abempty ; begins with "/" or is empty
187 * / path-absolute ; begins with "/" but not "//"
188 * / path-noscheme ; begins with a non-colon segment
189 * / path-rootless ; begins with a segment
190 * / path-empty ; zero characters
191 *
192 * path-abempty = *( "/" segment )
193 * path-absolute = "/" [ segment-nz *( "/" segment ) ]
194 * path-noscheme = segment-nz-nc *( "/" segment )
195 * path-rootless = segment-nz *( "/" segment )
196 * path-empty = 0<pchar>
197 *
198 * segment = *pchar
199 * segment-nz = 1*pchar
200 * segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" ) ; non-zero-length segment without any colon ":"
201 *
202 * pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
203 */
204 static final String pcharLegal = "-._~!$&'()*+,;=:@";
205 static final String segmentNzNcLegal = "-._~!$&'()*+,;=@";
206 static final String segmentLegal = pcharLegal;
207 static final String pathLegal = segmentLegal + "/";
208
209 /* query = *( pchar / "/" / "?" )
210 *
211 * fragment = *( pchar / "/" / "?" )
212 */
213 static final String queryFragLegal = pcharLegal + "/?";
214
215 private final static char a = 'a';
216 private final static char z = 'z';
217 private final static char A = 'A';
218 private final static char Z = 'Z';
219 private final static char upperCaseBitwiseMask = 0xdf;
220 private final static char lowerCaseBitwiseMask = 0x20;
221
222 static String toAsciiUpperCase(String s){
223 return new String(toAsciiUpperCase(s.toCharArray()));
224 }
225
226 static char [] toAsciiUpperCase(char [] array){
227 int length = array.length;
228 for (int i = 0; i < length ; i++){
229 if (array[i] >= a && array[i] <= z) {
230 array[i] = toAsciiUpperCase(array[i]);
231 }
232 }
233 return array;
234 }
235
236 static char toAsciiUpperCase(char c){
237 return (char) (c & upperCaseBitwiseMask);
238 }
239
240 static String toAsciiLowerCase(String s){
241 return new String(toAsciiLowerCase(s.toCharArray()));
242 }
243
244 static char[] toAsciiLowerCase(char [] array){
245 int length = array.length;
246 for (int i = 0; i < length ; i++){
247 if (array[i] >= A && array[i] <= Z) {
248 array[i] = toAsciiLowerCase(array[i]);
249 }
250 }
251 return array;
252 }
253
254 static char toAsciiLowerCase(char c){
255 return (char) (c | lowerCaseBitwiseMask);
256 }
257
258 static boolean charArraysEqual( char [] a , char [] b){
259 int alen = a.length;
260 int blen = b.length;
261 if (alen != blen) return false;
262 for (int i = 0; i < alen; i++){
263 if (a[i] != b[i]) return false;
264 }
265 return true;
266 }
267
268 static boolean asciiStringsUpperCaseEqual(String a, String b){
269 char [] ac = a.toCharArray();
270 toAsciiUpperCase(ac);
271 char [] bc = b.toCharArray();
272 toAsciiUpperCase(bc);
273 return charArraysEqual(ac, bc);
274 }
275
276 static boolean asciiStringsLowerCaseEqual(String a, String b){
277 char [] ac = a.toCharArray();
278 toAsciiLowerCase(ac);
279 char [] bc = b.toCharArray();
280 toAsciiLowerCase(bc);
281 return charArraysEqual(ac, bc);
282 }
283 /** Fixes windows file URI string by converting back slashes to forward
284 * slashes and inserting a forward slash before the drive letter if it is
285 * missing. No normalisation or modification of case is performed.
286 * @param uri String representation of URI
287 * @return fixed URI String
288 */
289 public static String fixWindowsURI(String uri) {
290 if (uri == null) return null;
291 if (File.separatorChar != '\\') return uri;
292 if ( uri.startsWith("file:") || uri.startsWith("FILE:")){
293 char [] u = uri.toCharArray();
294 int l = u.length;
295 StringBuilder sb = new StringBuilder(uri.length()+1);
296 for (int i=0; i<l; i++){
297 // Ensure we use forward slashes
298 if (u[i] == File.separatorChar) {
299 sb.append('/');
300 continue;
301 }
302 if (i == 5 && uri.startsWith(":", 6 )) {
303 // Windows drive letter without leading slashes doesn't comply
304 // with URI spec, fix it here
305 sb.append("/");
306 }
307 sb.append(u[i]);
308 }
309 return sb.toString();
310 }
311 return uri;
312 }
313
314 public static URI uriToURI(Uri uri){
315 return URI.create(uri.toString());
316 }
317
318 public static Uri urlToUri(URL url) throws URISyntaxException{
319 return Uri.parseAndCreate(fixWindowsURI(url.toString()));
320 }
321
322 public static File uriToFile(Uri uri){
323 return new File(uriToURI(uri));
324 }
325
326 public static Uri fileToUri(File file) throws URISyntaxException{
327 String path = file.getAbsolutePath();
328 if (File.separatorChar == '\\') {
329 path = path.replace(File.separatorChar, '/');
330 }
331 path = fixWindowsURI("file:" + path);
332 return Uri.escapeAndCreate(path); //$NON-NLS-1$
333 }
334
335 public static Uri filePathToUri(String path) throws URISyntaxException{
336 String forwardSlash = "/";
337 if (path == null || path.length() == 0) {
338 // codebase is "file:"
339 path = "*";
340 }
341 // Ensure compatibility with URLClassLoader, when directory
342 // character is dropped by File.
343 boolean directory = false;
344 if (path.endsWith(forwardSlash)) directory = true;
345 path = new File(path).getAbsolutePath();
346 if (directory) {
347 if (!(path.endsWith(File.separator))){
348 path = path + File.separator;
349 }
350 }
351 if (File.separatorChar == '\\') {
352 path = path.replace(File.separatorChar, '/');
353 }
354 path = fixWindowsURI("file:" + path);
355 return Uri.escapeAndCreate(path); //$NON-NLS-1$
356 }
357
358 /* Begin Object Implementation */
359
360 private final String string;
361 private final String scheme;
362 private final String schemespecificpart;
363 private final String authority;
364 private final String userinfo;
365 private final String host;
366 private final int port;
367 private final String path;
368 private final String query;
369 private final String fragment;
370 private final boolean opaque;
371 private final boolean absolute;
372 private final boolean serverAuthority;
373 private final String hashString;
374 private final int hash;
375 private final boolean fileSchemeCaseInsensitiveOS;
376
377 /**
378 *
379 * @param string
380 * @param scheme
381 * @param schemespecificpart
382 * @param authority
383 * @param userinfo
384 * @param host
385 * @param port
386 * @param path
387 * @param query
388 * @param fragment
389 * @param opaque
390 * @param absolute
391 * @param serverAuthority
392 * @param hash
393 */
394 private Uri(String string,
395 String scheme,
396 String schemespecificpart,
397 String authority,
398 String userinfo,
399 String host,
400 int port,
401 String path,
402 String query,
403 String fragment,
404 boolean opaque,
405 boolean absolute,
406 boolean serverAuthority,
407 int hash,
408 boolean fileSchemeCaseInsensitiveOS)
409 {
410 super();
411 this.scheme = scheme;
412 this.schemespecificpart = schemespecificpart;
413 this.authority = authority;
414 this.userinfo = userinfo;
415 this.host = host;
416 this.port = port;
417 this.path = path;
418 this.query = query;
419 this.fragment = fragment;
420 this.opaque = opaque;
421 this.absolute = absolute;
422 this.serverAuthority = serverAuthority;
423 if (string == null) {
424 StringBuilder result = new StringBuilder();
425 if (scheme != null) {
426 result.append(scheme);
427 result.append(':');
428 }
429 if (opaque) {
430 result.append(schemespecificpart);
431 } else {
432 if (authority != null) {
433 result.append("//"); //$NON-NLS-1$
434 result.append(authority);
435 }
436
437 if (path != null) {
438 result.append(path);
439 }
440
441 if (query != null) {
442 result.append('?');
443 result.append(query);
444 }
445 }
446
447 if (fragment != null) {
448 result.append('#');
449 result.append(fragment);
450 }
451
452 this.string = result.toString();
453 } else {
454 this.string = string;
455 }
456 this.hashString = getHashString();
457 this.hash = hash == -1 ? hashString.hashCode(): hash;
458 this.fileSchemeCaseInsensitiveOS = fileSchemeCaseInsensitiveOS;
459 }
460
461 /**
462 * Private constructor that doesn't throw URISyntaxException, all public
463 * constructors are designed to avoid finalizer attacks by calling static
464 * methods that throw URISyntaxException, just in case we
465 * decide to make this class non final at some point in future.
466 * @param p
467 */
468 private Uri(UriParser p){
469 this(p.string,
470 p.scheme,
471 p.schemespecificpart,
472 p.authority,
473 p.userinfo,
474 p.host,
475 p.port,
476 p.path,
477 p.query,
478 p.fragment,
479 p.opaque,
480 p.absolute,
481 p.serverAuthority,
482 p.hash,
483 p.fileSchemeCaseInsensitiveOS);
484 }
485
486 /**
487 * Creates a new URI instance according to the given string {@code uri}.
488 *
489 * The URI must strictly conform to RFC3986, it doesn't support extended
490 * characters sets like java.net.URI, instead all non ASCII characters
491 * must be escaped.
492 *
493 * Any encoded unreserved characters are decoded.
494 *
495 * @param uri
496 * the textual URI representation to be parsed into a URI object.
497 * @throws URISyntaxException
498 * if the given string {@code uri} doesn't fit to the
499 * specification RF3986 or could not be parsed correctly.
500 */
501 public Uri(String uri) throws URISyntaxException {
502 this(constructor1(uri));
503 }
504
505 private static UriParser constructor1(String uri) throws URISyntaxException {
506 uri = URIEncoderDecoder.decodeUnreserved(uri);
507 UriParser p = new UriParser();
508 p.parseURI(uri, false);
509 return p;
510 }
511
512 /**
513 * Creates a new URI instance using the given arguments. This constructor
514 * first creates a temporary URI string from the given components. This
515 * string will be parsed later on to create the URI instance.
516 * <p>
517 * {@code [scheme:]scheme-specific-part[#fragment]}
518 *
519 * @param scheme
520 * the scheme part of the URI.
521 * @param ssp
522 * the scheme-specific-part of the URI.
523 * @param frag
524 * the fragment part of the URI.
525 * @throws URISyntaxException
526 * if the temporary created string doesn't fit to the
527 * specification RFC2396 or could not be parsed correctly.
528 */
529 public Uri(String scheme, String ssp, String frag) throws URISyntaxException {
530 this(constructor2(scheme, ssp, frag));
531 }
532
533 private static UriParser constructor2(String scheme, String ssp, String frag) throws URISyntaxException{
534 StringBuilder uri = new StringBuilder();
535 if (scheme != null) {
536 uri.append(scheme);
537 uri.append(':');
538 }
539 if (ssp != null) {
540 // QUOTE ILLEGAL CHARACTERS
541 uri.append(quoteComponent(ssp, allLegalUnescaped));
542 }
543 if (frag != null) {
544 uri.append('#');
545 // QUOTE ILLEGAL CHARACTERS
546 uri.append(quoteComponent(frag, Uri.queryFragLegal));
547 }
548
549 UriParser p = new UriParser();
550 p.parseURI(uri.toString(), false);
551 return p;
552 }
553
554 /**
555 * Creates a new URI instance using the given arguments. This constructor
556 * first creates a temporary URI string from the given components. This
557 * string will be parsed later on to create the URI instance.
558 * <p>
559 * {@code [scheme:][user-info@]host[:port][path][?query][#fragment]}
560 *
561 * @param scheme
562 * the scheme part of the URI.
563 * @param userinfo
564 * the user information of the URI for authentication and
565 * authorization.
566 * @param host
567 * the host name of the URI.
568 * @param port
569 * the port number of the URI.
570 * @param path
571 * the path to the resource on the host.
572 * @param query
573 * the query part of the URI to specify parameters for the
574 * resource.
575 * @param fragment
576 * the fragment part of the URI.
577 * @throws URISyntaxException
578 * if the temporary created string doesn't fit to the
579 * specification RFC2396 or could not be parsed correctly.
580 */
581 public Uri(String scheme, String userinfo, String host, int port,
582 String path, String query, String fragment)
583 throws URISyntaxException {
584 this(constructor3(scheme, userinfo, host, port, path, query, fragment));
585 }
586
587 private static UriParser constructor3(String scheme, String userinfo, String host, int port,
588 String path, String query, String fragment) throws URISyntaxException {
589 if (scheme == null && userinfo == null && host == null && path == null
590 && query == null && fragment == null) {
591 UriParser p = new UriParser();
592 p.path = ""; //$NON-NLS-1$
593 return p;
594 }
595
596 if (scheme != null && path != null && path.length() > 0
597 && path.charAt(0) != '/') {
598 throw new URISyntaxException(path, Messages.getString("luni.82")); //$NON-NLS-1$
599 }
600
601 StringBuilder uri = new StringBuilder();
602 if (scheme != null) {
603 uri.append(scheme);
604 uri.append(':');
605 }
606
607 if (userinfo != null || host != null || port != -1) {
608 uri.append("//"); //$NON-NLS-1$
609 }
610
611 if (userinfo != null) {
612 // QUOTE ILLEGAL CHARACTERS in userinfo
613 uri.append(quoteComponent(userinfo, Uri.userinfoLegal));
614 uri.append('@');
615 }
616
617 if (host != null) {
618 // check for ipv6 addresses that hasn't been enclosed
619 // in square brackets
620 if (host.indexOf(':') != -1 && host.indexOf(']') == -1
621 && host.indexOf('[') == -1) {
622 host = "[" + host + "]"; //$NON-NLS-1$ //$NON-NLS-2$
623 }
624 uri.append(host);
625 }
626
627 if (port != -1) {
628 uri.append(':');
629 uri.append(port);
630 }
631
632 if (path != null) {
633 // QUOTE ILLEGAL CHARS
634 uri.append(quoteComponent(path, "/@" + Uri.pathLegal)); //$NON-NLS-1$
635 }
636
637 if (query != null) {
638 uri.append('?');
639 // QUOTE ILLEGAL CHARS
640 uri.append(quoteComponent(query, Uri.queryFragLegal));
641 }
642
643 if (fragment != null) {
644 // QUOTE ILLEGAL CHARS
645 uri.append('#');
646 uri.append(quoteComponent(fragment, Uri.queryFragLegal));
647 }
648
649 UriParser p = new UriParser();
650 p.parseURI(uri.toString(), true);
651 return p;
652 }
653
654 /**
655 * Creates a new URI instance using the given arguments. This constructor
656 * first creates a temporary URI string from the given components. This
657 * string will be parsed later on to create the URI instance.
658 * <p>
659 * {@code [scheme:]host[path][#fragment]}
660 *
661 * @param scheme
662 * the scheme part of the URI.
663 * @param host
664 * the host name of the URI.
665 * @param path
666 * the path to the resource on the host.
667 * @param fragment
668 * the fragment part of the URI.
669 * @throws URISyntaxException
670 * if the temporary created string doesn't fit to the
671 * specification RFC2396 or could not be parsed correctly.
672 */
673 public Uri(String scheme, String host, String path, String fragment)
674 throws URISyntaxException {
675 this(scheme, null, host, -1, path, null, fragment);
676 }
677
678 /**
679 * Creates a new URI instance using the given arguments. This constructor
680 * first creates a temporary URI string from the given components. This
681 * string will be parsed later on to create the URI instance.
682 * <p>
683 * {@code [scheme:][//authority][path][?query][#fragment]}
684 *
685 * @param scheme
686 * the scheme part of the URI.
687 * @param authority
688 * the authority part of the URI.
689 * @param path
690 * the path to the resource on the host.
691 * @param query
692 * the query part of the URI to specify parameters for the
693 * resource.
694 * @param fragment
695 * the fragment part of the URI.
696 * @throws URISyntaxException
697 * if the temporary created string doesn't fit to the
698 * specification RFC2396 or could not be parsed correctly.
699 */
700 public Uri(String scheme, String authority, String path, String query,
701 String fragment) throws URISyntaxException {
702 this(constructor4(scheme, authority, path, query, fragment));
703 }
704
705 private static UriParser constructor4(String scheme, String authority, String path, String query,
706 String fragment) throws URISyntaxException {
707 if (scheme != null && path != null && path.length() > 0
708 && path.charAt(0) != '/') {
709 throw new URISyntaxException(path, Messages.getString("luni.82")); //$NON-NLS-1$
710 }
711
712 StringBuilder uri = new StringBuilder();
713 if (scheme != null) {
714 uri.append(scheme);
715 uri.append(':');
716 }
717 if (authority != null) {
718 uri.append("//"); //$NON-NLS-1$
719 // QUOTE ILLEGAL CHARS
720 uri.append(quoteComponent(authority, "@[]" + Uri.authorityLegal)); //$NON-NLS-1$
721 }
722
723 if (path != null) {
724 // QUOTE ILLEGAL CHARS
725 uri.append(quoteComponent(path, "/@" + Uri.pathLegal)); //$NON-NLS-1$
726 }
727 if (query != null) {
728 // QUOTE ILLEGAL CHARS
729 uri.append('?');
730 uri.append(quoteComponent(query, Uri.queryFragLegal));
731 }
732 if (fragment != null) {
733 // QUOTE ILLEGAL CHARS
734 uri.append('#');
735 uri.append(quoteComponent(fragment, Uri.queryFragLegal));
736 }
737
738 UriParser p = new UriParser();
739 p.parseURI(uri.toString(), false);
740 return p;
741 }
742
743 /*
744 * Quote illegal chars for each component, but not the others
745 *
746 * @param component java.lang.String the component to be converted @param
747 * legalset java.lang.String the legal character set allowed in the
748 * component s @return java.lang.String the converted string
749 */
750 private static String quoteComponent(String component, String legalset) {
751 try {
752 /*
753 * Use a different encoder than URLEncoder since: 1. chars like "/",
754 * "#", "@" etc needs to be preserved instead of being encoded, 2.
755 * UTF-8 char set needs to be used for encoding instead of default
756 * platform one
757 */
758 return URIEncoderDecoder.quoteIllegal(component, legalset);
759 } catch (UnsupportedEncodingException e) {
760 throw new RuntimeException(e.toString());
761 }
762 }
763
764 /**
765 * Compares this URI with the given argument {@code uri}. This method will
766 * return a negative value if this URI instance is less than the given
767 * argument and a positive value if this URI instance is greater than the
768 * given argument. The return value {@code 0} indicates that the two
769 * instances represent the same URI. To define the order the single parts of
770 * the URI are compared with each other. String components will be orderer
771 * in the natural case-sensitive way. A hierarchical URI is less than an
772 * opaque URI and if one part is {@code null} the URI with the undefined
773 * part is less than the other one.
774 *
775 * @param uri
776 * the URI this instance has to compare with.
777 * @return the value representing the order of the two instances.
778 */
779 @Override
780 public int compareTo(Uri uri) {
781 int ret;
782
783 // compare schemes
784 if (scheme == null && uri.scheme != null) {
785 return -1;
786 } else if (scheme != null && uri.scheme == null) {
787 return 1;
788 } else if (scheme != null && uri.scheme != null) {
789 ret = scheme.compareToIgnoreCase(uri.scheme);
790 if (ret != 0) return ret;
791 }
792
793 // compare opacities
794 if (!opaque && uri.opaque) {
795 return -1;
796 } else if (opaque && !uri.opaque) {
797 return 1;
798 } else if (opaque && uri.opaque) {
799 ret = schemespecificpart.compareTo(uri.schemespecificpart);
800 if (ret != 0) {
801 return ret;
802 }
803 } else {
804
805 // otherwise both must be hierarchical
806
807 // compare authorities
808 if (authority != null && uri.authority == null) {
809 return 1;
810 } else if (authority == null && uri.authority != null) {
811 return -1;
812 } else if (authority != null && uri.authority != null) {
813 if (host != null && uri.host != null) {
814 // both are server based, so compare userinfo, host, port
815 if (userinfo != null && uri.userinfo == null) {
816 return 1;
817 } else if (userinfo == null && uri.userinfo != null) {
818 return -1;
819 } else if (userinfo != null && uri.userinfo != null) {
820 ret = userinfo.compareTo(uri.userinfo);
821 if (ret != 0) {
822 return ret;
823 }
824 }
825
826 // userinfo's are the same, compare hostname
827 ret = host.compareToIgnoreCase(uri.host);
828 if (ret != 0) {
829 return ret;
830 }
831
832 // compare port
833 if (port != uri.port) {
834 return port - uri.port;
835 }
836 } else { // one or both are registry based, compare the whole
837 // authority
838 ret = authority.compareTo(uri.authority);
839 if (ret != 0) {
840 return ret;
841 }
842 }
843 }
844
845 // authorities are the same
846 // compare paths
847
848 if (fileSchemeCaseInsensitiveOS){
849 ret = toAsciiUpperCase(path).compareTo(toAsciiUpperCase(uri.path));
850 // ret = path.toUpperCase(Locale.ENGLISH).compareTo(uri.path.toUpperCase(Locale.ENGLISH));
851 } else {
852 ret = path.compareTo(uri.path);
853 }
854 if (ret != 0) {
855 return ret;
856 }
857
858 // compare queries
859
860 if (query != null && uri.query == null) {
861 return 1;
862 } else if (query == null && uri.query != null) {
863 return -1;
864 } else if (query != null && uri.query != null) {
865 ret = query.compareTo(uri.query);
866 if (ret != 0) {
867 return ret;
868 }
869 }
870 }
871
872 // everything else is identical, so compare fragments
873 if (fragment != null && uri.fragment == null) {
874 return 1;
875 } else if (fragment == null && uri.fragment != null) {
876 return -1;
877 } else if (fragment != null && uri.fragment != null) {
878 ret = fragment.compareTo(uri.fragment);
879 if (ret != 0) {
880 return ret;
881 }
882 }
883
884 // identical
885 return 0;
886 }
887
888 /**
889 * Parses the given argument {@code rfc3986compliantURI} and creates an appropriate URI
890 * instance.
891 *
892 * The parameter string is checked for compliance, an IllegalArgumentException
893 * is thrown if the string is non compliant.
894 *
895 * @param rfc3986compliantURI
896 * the string which has to be parsed to create the URI instance.
897 * @return the created instance representing the given URI.
898 */
899 public static Uri create(String rfc3986compliantURI) {
900 Uri result = null;
901 try {
902 result = new Uri(rfc3986compliantURI);
903 } catch (URISyntaxException e) {
904 throw new IllegalArgumentException(e.getMessage());
905 }
906 return result;
907 }
908
909 /**
910 * The parameter string doesn't contain any existing escape sequences, any
911 * escape character % found is encoded as %25. Illegal characters are
912 * escaped if possible.
913 *
914 * The Uri is normalised according to RFC3986.
915 *
916 * @param unescapedString URI in un-escaped string form
917 * @return an RFC3986 compliant Uri.
918 * @throws java.net.URISyntaxException if string cannot be escaped.
919 */
920 public static Uri escapeAndCreate(String unescapedString) throws URISyntaxException{
921 return new Uri(quoteComponent(unescapedString, allLegalUnescaped));
922 }
923
924 /**
925 * The parameter string may already contain escaped sequences, any illegal
926 * characters are escaped and any that shouldn't be escaped are un-escaped.
927 *
928 * The escape character % is not re-encoded.
929 * @param nonCompliantEscapedString URI in string from.
930 * @return an RFC3986 compliant Uri.
931 * @throws java.net.URISyntaxException if string cannot be escaped.
932 */
933 public static Uri parseAndCreate(String nonCompliantEscapedString) throws URISyntaxException{
934 return new Uri(quoteComponent(nonCompliantEscapedString, allLegal));
935 }
936
937
938 /*
939 * Takes a string that may contain hex sequences like %F1 or %2b and
940 * converts the hex values following the '%' to uppercase
941 */
942 private String convertHexToUpperCase(String s) {
943 StringBuilder result = new StringBuilder(""); //$NON-NLS-1$
944 if (s.indexOf('%') == -1) {
945 return s;
946 }
947
948 int index, previndex = 0;
949 while ((index = s.indexOf('%', previndex)) != -1) {
950 result.append(s.substring(previndex, index + 1));
951 // Convert to upper case ascii
952 result.append(toAsciiUpperCase(s.substring(index + 1, index + 3).toCharArray()));
953 index += 3;
954 previndex = index;
955 }
956 return result.toString();
957 }
958
959 /*
960 * Takes two strings that may contain hex sequences like %F1 or %2b and
961 * compares them, ignoring case for the hex values. Hex values must always
962 * occur in pairs as above
963 */
964 private boolean equalsHexCaseInsensitive(String first, String second) {
965 //Hex will always be upper case.
966 if (first != null) return first.equals(second);
967 return second == null;
968 }
969
970 /**
971 * Compares this URI instance with the given argument {@code o} and
972 * determines if both are equal. Two URI instances are equal if all single
973 * parts are identical in their meaning.
974 *
975 * @param o
976 * the URI this instance has to be compared with.
977 * @return {@code true} if both URI instances point to the same resource,
978 * {@code false} otherwise.
979 */
980 @Override
981 public boolean equals(Object o) {
982 if (!(o instanceof Uri)) {
983 return false;
984 }
985 if (hash != o.hashCode()) return false;
986 Uri uri = (Uri) o;
987
988 if (uri.fragment == null && fragment != null || uri.fragment != null
989 && fragment == null) {
990 return false;
991 } else if (uri.fragment != null && fragment != null) {
992 if (!equalsHexCaseInsensitive(uri.fragment, fragment)) {
993 return false;
994 }
995 }
996
997 if (uri.scheme == null && scheme != null || uri.scheme != null
998 && scheme == null) {
999 return false;
1000 } else if (uri.scheme != null && scheme != null) {
1001 if (!uri.scheme.equalsIgnoreCase(scheme)) {
1002 return false;
1003 }
1004 }
1005
1006 if (uri.opaque && opaque) {
1007 return equalsHexCaseInsensitive(uri.schemespecificpart,
1008 schemespecificpart);
1009 } else if (!uri.opaque && !opaque) {
1010 if ( !(path != null && (path.equals(uri.path)
1011 || fileSchemeCaseInsensitiveOS
1012 // Upper case comparison required for Windows & VMS.
1013 && asciiStringsUpperCaseEqual(path, uri.path)
1014 )))
1015 {
1016 return false;
1017 }
1018
1019 if (uri.query != null && query == null || uri.query == null
1020 && query != null) {
1021 return false;
1022 } else if (uri.query != null && query != null) {
1023 if (!equalsHexCaseInsensitive(uri.query, query)) {
1024 return false;
1025 }
1026 }
1027
1028 if (uri.authority != null && authority == null
1029 || uri.authority == null && authority != null) {
1030 return false;
1031 } else if (uri.authority != null && authority != null) {
1032 if (uri.host != null && host == null || uri.host == null
1033 && host != null) {
1034 return false;
1035 } else if (uri.host == null && host == null) {
1036 // both are registry based, so compare the whole authority
1037 return equalsHexCaseInsensitive(uri.authority, authority);
1038 } else { // uri.host != null && host != null, so server-based
1039 if (!host.equalsIgnoreCase(uri.host)) {
1040 return false;
1041 }
1042
1043 if (port != uri.port) {
1044 return false;
1045 }
1046
1047 if (uri.userinfo != null && userinfo == null
1048 || uri.userinfo == null && userinfo != null) {
1049 return false;
1050 } else if (uri.userinfo != null && userinfo != null) {
1051 return equalsHexCaseInsensitive(userinfo, uri.userinfo);
1052 } else {
1053 return true;
1054 }
1055 }
1056 } else {
1057 // no authority
1058 return true;
1059 }
1060
1061 } else {
1062 // one is opaque, the other hierarchical
1063 return false;
1064 }
1065 }
1066
1067 /**
1068 * <p>
1069 * Indicates whether the specified Uri is implied by this {@link
1070 * Uri}. Returns {@code true} if all of the following conditions are
1071 * {@code true}, otherwise {@code false}:
1072 * </p>
1073 * <ul>
1074 * <li>this scheme is not {@code null}
1075 * <li>this scheme is equal to {@code implied}'s scheme.
1076 * <li>if this host is not {@code null}, the
1077 * following conditions are checked
1078 * <ul>
1079 * <li>{@code cs}'s host is not {@code null}
1080 * <li>the wildcard or partial wildcard of this {@code Uri}'s
1081 * host matches {@code implied}'s host.
1082 * </ul>
1083 * <li>if this {@code Uri}'s port != -1 the port of {@code
1084 * implied}'s location is equal to this {@code Uri}'s port
1085 * <li>this {@code Uri}'s path matches {@code implied}'s path
1086 * whereas special wildcard matching applies as described below.
1087 * </ul>
1088 * Matching rules for path:
1089 * <ul>
1090 * <li>if this {@code Uri}'s path ends with {@code "/-"},
1091 * then {@code implied}'s path must start with {@code Uri}'s path
1092 * (exclusive the trailing '-')
1093 * <li>if this {@code Uri}'s path ends with {@code "/*"},
1094 * then {@code implied}'s path must start with {@code Uri}'s path
1095 * (exclusive the trailing '*') and must not have any further '/'
1096 * <li>if this {@code Uri}'s path ends with {@code "/"},
1097 * then {@code implied}'s path must start with {@code Uri}'s path
1098 * <li>if this {@code Uri}'s path does not end with {@code
1099 * "/"}, then {@code implied}'s path must start with {@code Uri}'s
1100 * path with the '/' appended to it.
1101 * </ul>
1102 * Examples for Uri that imply the Uri
1103 * "http://harmony.apache.org/milestones/M9/apache-harmony.jar":
1104 *
1105 * <pre>
1106 * http:
1107 * http://*/milestones/M9/*
1108 * http://*.apache.org/milestones/M9/*
1109 * http://harmony.apache.org/milestones/-
1110 * http://harmony.apache.org/milestones/M9/apache-harmony.jar
1111 * </pre>
1112 *
1113 * @param implied
1114 * the Uri to check.
1115 * @return {@code true} if the argument is implied by this
1116 * {@code Uri}, otherwise {@code false}.
1117 */
1118 public boolean implies(Uri implied) { // package private for junit
1119 //.This section of code was copied from Apache Harmony's CodeSource
1120 // SVN Revision 929252
1121 //
1122 // Here, javadoc:N refers to the appropriate item in the API spec for
1123 // the CodeSource.implies()
1124 // The info was taken from the 1.5 final API spec
1125
1126 // javadoc:1
1127 // if (cs == null) {
1128 // return false;
1129 // }
1130
1131
1132 // javadoc:2
1133 // with a comment: the javadoc says only about certificates and does
1134 // not explicitly mention CodeSigners' certs.
1135 // It seems more convenient to use getCerts() to get the real
1136 // certificates - with a certificates got form the signers
1137 // Certificate[] thizCerts = getCertificatesNoClone();
1138 // if (thizCerts != null) {
1139 // Certificate[] thatCerts = cs.getCertificatesNoClone();
1140 // if (thatCerts == null
1141 // || !PolicyUtils.matchSubset(thizCerts, thatCerts)) {
1142 // return false;
1143 // }
1144 // }
1145
1146 // javadoc:3
1147
1148
1149 //javadoc:3.1
1150 // URL otherURL = cs.getLocation();
1151 // if ( otherURL == null) {
1152 // return false;
1153 // }
1154 // URI otherURI;
1155 // try {
1156 // otherURI = otherURL.toURI();
1157 // } catch (URISyntaxException ex) {
1158 // return false;
1159 // }
1160 //javadoc:3.2
1161 if (hash == implied.hash){
1162 if (this.equals(implied)) {
1163 return true;
1164 }
1165 }
1166 //javadoc:3.3
1167 if ( scheme != null){
1168 if (!scheme.equalsIgnoreCase(implied.scheme)) {
1169 return false;
1170 }
1171 }
1172 //javadoc:3.4
1173 if (host != null) {
1174 if (implied.host == null) {
1175 return false;
1176 }
1177
1178 // 1. According to the spec, an empty string will be considered
1179 // as "localhost" in the SocketPermission
1180 // 2. 'file://' URLs will have an empty getHost()
1181 // so, let's make a special processing of localhost-s, I do
1182 // believe this'll improve performance of file:// code sources
1183
1184 //
1185 // Don't have to evaluate both the boolean-s each time.
1186 // It's better to evaluate them directly under if() statement.
1187 //
1188 // boolean thisIsLocalHost = thisHost.length() == 0 || "localhost".equals(thisHost);
1189 // boolean thatIsLocalHost = thatHost.length() == 0 || "localhost".equals(thatHost);
1190 //
1191 // if( !(thisIsLocalHost && thatIsLocalHost) &&
1192 // !thisHost.equals(thatHost)) {
1193
1194 if (!((host.length() == 0 || "localhost".equals(host)) && (implied.host //$NON-NLS-1$
1195 .length() == 0 || "localhost".equals(implied.host))) //$NON-NLS-1$
1196 && !host.equals(implied.host)) {
1197
1198 // Do wildcard matching here to replace SocketPermission functionality.
1199 // This section was copied from Apache Harmony SocketPermission
1200 boolean hostNameMatches = false;
1201 boolean isPartialWild = (host.charAt(0) == '*');
1202 if (isPartialWild) {
1203 boolean isWild = (host.length() == 1);
1204 if (isWild) {
1205 hostNameMatches = true;
1206 } else {
1207 // Check if thisHost matches the end of thatHost after the wildcard
1208 int length = host.length() - 1;
1209 hostNameMatches = implied.host.regionMatches(implied.host.length() - length,
1210 host, 1, length);
1211 }
1212 }
1213 if (!hostNameMatches) return false; // else continue.
1214
1215 /* Don't want to try resolving URI with DNS, it either has a
1216 * matching host or it doesn't.
1217 *
1218 * The following section is for resolving hosts, it is
1219 * not relevant here, but has been preserved for information
1220 * purposes only.
1221 *
1222 * Not only is it expensive to perform DNS resolution, hence
1223 * the creation of Uri, but a CodeSource.implies
1224 * may also require another SocketPermission which may
1225 * cause the policy to get stuck in an endless loop, since it
1226 * doesn't perform the implies in priviledged mode, it might
1227 * also allow an attacker to substitute one codebase for
1228 * another using a dns cache poisioning attack. In any case
1229 * the DNS cannot be assumed trustworthy enough to supply
1230 * the policy with information at this level. The implications
1231 * are greater than the threat posed by SocketPermission
1232 * which simply allows a network connection, as this may
1233 * apply to any Permission, even AllPermission.
1234 *
1235 * Typically the URI of the codebase will be a match for
1236 * the codebase annotation string that is stored as a URL
1237 * in CodeSource, then converted to a URI for comparison.
1238 */
1239
1240 // Obvious, but very slow way....
1241 //
1242 // SocketPermission thisPerm = new SocketPermission(
1243 // this.location.getHost(), "resolve");
1244 // SocketPermission thatPerm = new SocketPermission(
1245 // cs.location.getHost(), "resolve");
1246 // if (!thisPerm.implies(thatPerm)) {
1247 // return false;
1248 // }
1249 //
1250 // let's cache it:
1251
1252 // if (this.sp == null) {
1253 // this.sp = new SocketPermission(thisHost, "resolve"); //$NON-NLS-1$
1254 // }
1255 //
1256 // if (cs.sp == null) {
1257 // cs.sp = new SocketPermission(thatHost, "resolve"); //$NON-NLS-1$
1258 // }
1259 //
1260 // if (!this.sp.implies(cs.sp)) {
1261 // return false;
1262 // }
1263
1264 } // if( ! this.location.getHost().equals(cs.location.getHost())
1265 } // if (this.location.getHost() != null)
1266
1267 //javadoc:3.5
1268 if (port != -1) {
1269 if (port != implied.port) {
1270 return false;
1271 }
1272 }
1273
1274 //javadoc:3.6
1275 // compatbility with URL.getFile
1276 String thisFile;
1277 String thatFile;
1278 if (fileSchemeCaseInsensitiveOS){
1279 thisFile = path == null ? null: toAsciiUpperCase(path);
1280 thatFile = implied.path == null ? null: toAsciiUpperCase(implied.path);
1281 } else {
1282 thisFile = path;
1283 thatFile = implied.path;
1284 }
1285 if (thatFile == null || thisFile == null) return false;
1286 if (thisFile.endsWith("/-")) { //javadoc:3.6."/-" //$NON-NLS-1$
1287 if (!thatFile.startsWith(thisFile.substring(0, thisFile
1288 .length() - 2))) {
1289 return false;
1290 }
1291 } else if (thisFile.endsWith("/*")) { //javadoc:3.6."/*" //$NON-NLS-1$
1292 if (!thatFile.startsWith(thisFile.substring(0, thisFile
1293 .length() - 2))) {
1294 return false;
1295 }
1296 // no further separators(s) allowed
1297 if (thatFile.indexOf("/", thisFile.length() - 1) != -1) { //$NON-NLS-1$
1298 return false;
1299 }
1300 } else {
1301 // javadoc:3.6."/"
1302 if (!thisFile.equals(thatFile)) {
1303 if (!thisFile.endsWith("/")) { //$NON-NLS-1$
1304 if (!thatFile.equals(thisFile + "/")) { //$NON-NLS-1$
1305 return false;
1306 }
1307 } else {
1308 return false;
1309 }
1310 }
1311 }
1312
1313 // Fragment and path are ignored.
1314 //javadoc:3.7
1315 // A URL Anchor is a URI Fragment.
1316 // if (thiss.getFragment() != null) {
1317 // if (!thiss.getFragment().equals(implied.getFragment())) {
1318 // return false;
1319 // }
1320 // }
1321 // ok, every check was made, and they all were successful.
1322 // it's ok to return true.
1323
1324
1325 // javadoc: a note about CodeSource with null location and null Certs
1326 // is applicable here
1327 return true;
1328 }
1329
1330 /**
1331 * Gets the decoded authority part of this URI.
1332 *
1333 * @return the decoded authority part or {@code null} if undefined.
1334 */
1335 public String getAuthority() {
1336 return decode(authority);
1337 }
1338
1339 /**
1340 * Gets the decoded fragment part of this URI.
1341 *
1342 * @return the decoded fragment part or {@code null} if undefined.
1343 */
1344 public String getFragment() {
1345 return decode(fragment);
1346 }
1347
1348 /**
1349 * Gets the host part of this URI.
1350 *
1351 * @return the host part or {@code null} if undefined.
1352 */
1353 public String getHost() {
1354 return host;
1355 }
1356
1357 /**
1358 * Gets the decoded path part of this URI.
1359 *
1360 * @return the decoded path part or {@code null} if undefined.
1361 */
1362 public String getPath() {
1363 return decode(path);
1364 }
1365
1366 /**
1367 * Gets the port number of this URI.
1368 *
1369 * @return the port number or {@code -1} if undefined.
1370 */
1371 public int getPort() {
1372 return port;
1373 }
1374
1375 /**
1376 * Gets the decoded query part of this URI.
1377 *
1378 * @return the decoded query part or {@code null} if undefined.
1379 */
1380 public String getQuery() {
1381 return decode(query);
1382 }
1383
1384 /**
1385 * Gets the authority part of this URI in raw form.
1386 *
1387 * @return the encoded authority part or {@code null} if undefined.
1388 */
1389 public String getRawAuthority() {
1390 return authority;
1391 }
1392
1393 /**
1394 * Gets the fragment part of this URI in raw form.
1395 *
1396 * @return the encoded fragment part or {@code null} if undefined.
1397 */
1398 public String getRawFragment() {
1399 return fragment;
1400 }
1401
1402 /**
1403 * Gets the path part of this URI in raw form.
1404 *
1405 * @return the encoded path part or {@code null} if undefined.
1406 */
1407 public String getRawPath() {
1408 return path;
1409 }
1410
1411 /**
1412 * Gets the query part of this URI in raw form.
1413 *
1414 * @return the encoded query part or {@code null} if undefined.
1415 */
1416 public String getRawQuery() {
1417 return query;
1418 }
1419
1420 /**
1421 * Gets the scheme-specific part of this URI in raw form.
1422 *
1423 * @return the encoded scheme-specific part or {@code null} if undefined.
1424 */
1425 public String getRawSchemeSpecificPart() {
1426 return schemespecificpart;
1427 }
1428
1429 /**
1430 * Gets the user-info part of this URI in raw form.
1431 *
1432 * @return the encoded user-info part or {@code null} if undefined.
1433 */
1434 public String getRawUserInfo() {
1435 return userinfo;
1436 }
1437
1438 /**
1439 * Gets the scheme part of this URI.
1440 *
1441 * @return the scheme part or {@code null} if undefined.
1442 */
1443 public String getScheme() {
1444 return scheme;
1445 }
1446
1447 /**
1448 * Gets the decoded scheme-specific part of this URI.
1449 *
1450 * @return the decoded scheme-specific part or {@code null} if undefined.
1451 */
1452 public String getSchemeSpecificPart() {
1453 return decode(schemespecificpart);
1454 }
1455
1456 /**
1457 * Gets the decoded user-info part of this URI.
1458 *
1459 * @return the decoded user-info part or {@code null} if undefined.
1460 */
1461 public String getUserInfo() {
1462 return decode(userinfo);
1463 }
1464
1465 /**
1466 * Gets the hashcode value of this URI instance.
1467 *
1468 * @return the appropriate hashcode value.
1469 */
1470 @Override
1471 public int hashCode() {
1472 return hash;
1473 }
1474
1475 /**
1476 * Indicates whether this URI is absolute, which means that a scheme part is
1477 * defined in this URI.
1478 *
1479 * @return {@code true} if this URI is absolute, {@code false} otherwise.
1480 */
1481 public boolean isAbsolute() {
1482 return absolute;
1483 }
1484
1485 /**
1486 * Indicates whether this URI is opaque or not. An opaque URI is absolute
1487 * and has a scheme-specific part which does not start with a slash
1488 * character. All parts except scheme, scheme-specific and fragment are
1489 * undefined.
1490 *
1491 * @return {@code true} if the URI is opaque, {@code false} otherwise.
1492 */
1493 public boolean isOpaque() {
1494 return opaque;
1495 }
1496
1497 /**
1498 * Normalizes the path part of this URI.
1499 *
1500 * @return an URI object which represents this instance with a normalized
1501 * path.
1502 */
1503 public Uri normalize() {
1504 if (opaque) {
1505 return this;
1506 }
1507 String normalizedPath = normalize(path);
1508 // if the path is already normalized, return this
1509 if (path.equals(normalizedPath)) {
1510 return this;
1511 }
1512 // get an exact copy of the URI re-calculate the scheme specific part
1513 // since the path of the normalized URI is different from this URI.
1514 return new Uri( null,
1515 scheme,
1516 setSchemeSpecificPart(authority, normalizedPath , query),
1517 authority,
1518 userinfo,
1519 host,
1520 port,
1521 normalizedPath,
1522 query,
1523 fragment,
1524 opaque,
1525 absolute,
1526 serverAuthority,
1527 hash,
1528 fileSchemeCaseInsensitiveOS);
1529 }
1530
1531
1532 /*
1533 * normalize path, and return the resulting string
1534 */
1535 private String normalize(String path) {
1536 // count the number of '/'s, to determine number of segments
1537 int index = -1;
1538 int pathlen = path.length();
1539 int size = 0;
1540 if (pathlen > 0 && path.charAt(0) != '/') {
1541 size++;
1542 }
1543 while ((index = path.indexOf('/', index + 1)) != -1) {
1544 if (index + 1 < pathlen && path.charAt(index + 1) != '/') {
1545 size++;
1546 }
1547 }
1548
1549 String[] seglist = new String[size];
1550 boolean[] include = new boolean[size];
1551
1552 // break the path into segments and store in the list
1553 int current = 0;
1554 int index2;
1555 index = (pathlen > 0 && path.charAt(0) == '/') ? 1 : 0;
1556 while ((index2 = path.indexOf('/', index + 1)) != -1) {
1557 seglist[current++] = path.substring(index, index2);
1558 index = index2 + 1;
1559 }
1560
1561 // if current==size, then the last character was a slash
1562 // and there are no more segments
1563 if (current < size) {
1564 seglist[current] = path.substring(index);
1565 }
1566
1567 // determine which segments get included in the normalized path
1568 for (int i = 0; i < size; i++) {
1569 include[i] = true;
1570 if (seglist[i].equals("..")) { //$NON-NLS-1$
1571 int remove = i - 1;
1572 // search back to find a segment to remove, if possible
1573 while (remove > -1 && !include[remove]) {
1574 remove--;
1575 }
1576 // if we find a segment to remove, remove it and the ".."
1577 // segment
1578 if (remove > -1 && !seglist[remove].equals("..")) { //$NON-NLS-1$
1579 include[remove] = false;
1580 include[i] = false;
1581 }
1582 } else if (seglist[i].equals(".")) { //$NON-NLS-1$
1583 include[i] = false;
1584 }
1585 }
1586
1587 // put the path back together
1588 StringBuilder newpath = new StringBuilder();
1589 if (path.startsWith("/")) { //$NON-NLS-1$
1590 newpath.append('/');
1591 }
1592
1593 for (int i = 0; i < seglist.length; i++) {
1594 if (include[i]) {
1595 newpath.append(seglist[i]);
1596 newpath.append('/');
1597 }
1598 }
1599
1600 // if we used at least one segment and the path previously ended with
1601 // a slash and the last segment is still used, then delete the extra
1602 // trailing '/'
1603 if (!path.endsWith("/") && seglist.length > 0 //$NON-NLS-1$
1604 && include[seglist.length - 1]) {
1605 newpath.deleteCharAt(newpath.length() - 1);
1606 }
1607
1608 String result = newpath.toString();
1609
1610 // check for a ':' in the first segment if one exists,
1611 // prepend "./" to normalize
1612 index = result.indexOf(':');
1613 index2 = result.indexOf('/');
1614 if (index != -1 && (index < index2 || index2 == -1)) {
1615 newpath.insert(0, "./"); //$NON-NLS-1$
1616 result = newpath.toString();
1617 }
1618 return result;
1619 }
1620
1621 /**
1622 * Tries to parse the authority component of this URI to divide it into the
1623 * host, port, and user-info. If this URI is already determined as a
1624 * ServerAuthority this instance will be returned without changes.
1625 *
1626 * @return this instance with the components of the parsed server authority.
1627 * @throws URISyntaxException
1628 * if the authority part could not be parsed as a server-based
1629 * authority.
1630 */
1631 public Uri parseServerAuthority() throws URISyntaxException {
1632 if (!serverAuthority) {
1633 UriParser p = new UriParser();
1634 p.parseURI(this.toString(), false);
1635 p.parseAuthority(true);
1636 return new Uri(p);
1637 }
1638 return this;
1639 }
1640
1641 /**
1642 * Makes the given URI {@code relative} to a relative URI against the URI
1643 * represented by this instance.
1644 *
1645 * @param relative
1646 * the URI which has to be relativized against this URI.
1647 * @return the relative URI.
1648 */
1649 public Uri relativize(Uri relative) {
1650 if (relative.opaque || opaque) {
1651 return relative;
1652 }
1653
1654 if (scheme == null ? relative.scheme != null : !scheme
1655 .equals(relative.scheme)) {
1656 return relative;
1657 }
1658
1659 if (authority == null ? relative.authority != null : !authority
1660 .equals(relative.authority)) {
1661 return relative;
1662 }
1663
1664 // normalize both paths
1665 String thisPath = normalize(path);
1666 String relativePath = normalize(relative.path);
1667
1668 /*
1669 * if the paths aren't equal, then we need to determine if this URI's
1670 * path is a parent path (begins with) the relative URI's path
1671 */
1672 if (!thisPath.equals(relativePath)) {
1673 // if this URI's path doesn't end in a '/', add one
1674 if (!thisPath.endsWith("/")) { //$NON-NLS-1$
1675 thisPath = thisPath + '/';
1676 }
1677 /*
1678 * if the relative URI's path doesn't start with this URI's path,
1679 * then just return the relative URI; the URIs have nothing in
1680 * common
1681 */
1682 if (!relativePath.startsWith(thisPath)) {
1683 return relative;
1684 }
1685 }
1686
1687 String qry = relative.query;
1688 // the result URI is the remainder of the relative URI's path
1689 String pth = relativePath.substring(thisPath.length());
1690 return new Uri( null,
1691 null,
1692 setSchemeSpecificPart(null, pth, qry),
1693 null,
1694 null,
1695 null,
1696 -1,
1697 pth,
1698 qry,
1699 relative.fragment,
1700 false,
1701 false,
1702 false,
1703 -1,
1704 fileSchemeCaseInsensitiveOS);
1705 }
1706
1707 /**
1708 * Resolves the given URI {@code relative} against the URI represented by
1709 * this instance.
1710 *
1711 * @param relative
1712 * the URI which has to be resolved against this URI.
1713 * @return the resolved URI.
1714 */
1715 public Uri resolve(Uri relative) {
1716 if (relative.absolute || opaque) {
1717 return relative;
1718 }
1719
1720 if (relative.path.equals("") && relative.scheme == null //$NON-NLS-1$
1721 && relative.authority == null && relative.query == null
1722 && relative.fragment != null) {
1723 // if the relative URI only consists of fragment,
1724 // the resolved URI is very similar to this URI,
1725 // except that it has the fragement from the relative URI.
1726
1727 return new Uri( null,
1728 scheme,
1729 schemespecificpart,
1730 authority,
1731 userinfo,
1732 host,
1733 port,
1734 path,
1735 query,
1736 relative.fragment,
1737 opaque,
1738 absolute,
1739 serverAuthority,
1740 hash,
1741 fileSchemeCaseInsensitiveOS);
1742 // no need to re-calculate the scheme specific part,
1743 // since fragment is not part of scheme specific part.
1744
1745 }
1746
1747 if (relative.authority != null) {
1748 // if the relative URI has authority,
1749 // the resolved URI is almost the same as the relative URI,
1750 // except that it has the scheme of this URI.
1751 return new Uri( null,
1752 scheme,
1753 relative.schemespecificpart,
1754 relative.authority,
1755 relative.userinfo,
1756 relative.host,
1757 relative.port,
1758 relative.path,
1759 relative.query,
1760 relative.fragment,
1761 relative.opaque,
1762 absolute,
1763 relative.serverAuthority,
1764 relative.hash,
1765 fileSchemeCaseInsensitiveOS);
1766 } else {
1767 // since relative URI has no authority,
1768 // the resolved URI is very similar to this URI,
1769 // except that it has the query and fragment of the relative URI,
1770 // and the path is different.
1771 // re-calculate the scheme specific part since
1772 // query and path of the resolved URI is different from this URI.
1773 int endindex = path.lastIndexOf('/') + 1;
1774 String p = relative.path.startsWith("/")? relative.path:
1775 normalize(path.substring(0, endindex) + relative.path);
1776 return new Uri( null,
1777 scheme,
1778 setSchemeSpecificPart(authority, p, relative.query),
1779 authority,
1780 userinfo,
1781 host,
1782 port,
1783 p,
1784 relative.query,
1785 relative.fragment,
1786 opaque,
1787 absolute,
1788 serverAuthority,
1789 hash,
1790 fileSchemeCaseInsensitiveOS);
1791 }
1792 }
1793
1794 /**
1795 * UriParser method used to re-calculate the scheme specific part of the
1796 * resolved or normalized URIs
1797 */
1798 private String setSchemeSpecificPart(String authority,
1799 String path,
1800 String query) {
1801 // ssp = [//authority][path][?query]
1802 StringBuilder ssp = new StringBuilder();
1803 if (authority != null) {
1804 ssp.append("//"); //$NON-NLS-1$
1805 ssp.append(authority);
1806 }
1807 if (path != null) {
1808 ssp.append(path);
1809 }
1810 if (query != null) {
1811 ssp.append("?"); //$NON-NLS-1$
1812 ssp.append(query);
1813 }
1814 // reset string, so that it can be re-calculated correctly when asked.
1815 return ssp.toString();
1816 }
1817
1818 /**
1819 * Creates a new URI instance by parsing the given string {@code relative}
1820 * and resolves the created URI against the URI represented by this
1821 * instance.
1822 *
1823 * @param relative
1824 * the given string to create the new URI instance which has to
1825 * be resolved later on.
1826 * @return the created and resolved URI.
1827 */
1828 public Uri resolve(String relative) {
1829 return resolve(create(relative));
1830 }
1831
1832 /*
1833 * Encode unicode chars that are not part of US-ASCII char set into the
1834 * escaped form
1835 *
1836 * i.e. The Euro currency symbol is encoded as "%E2%82%AC".
1837 *
1838 * @param component java.lang.String the component to be converted @param
1839 * legalset java.lang.String the legal character set allowed in the
1840 * component s @return java.lang.String the converted string
1841 */
1842 private String encodeOthers(String s) {
1843 try {
1844 /*
1845 * Use a different encoder than URLEncoder since: 1. chars like "/",
1846 * "#", "@" etc needs to be preserved instead of being encoded, 2.
1847 * UTF-8 char set needs to be used for encoding instead of default
1848 * platform one 3. Only other chars need to be converted
1849 */
1850 return URIEncoderDecoder.encodeOthers(s);
1851 } catch (UnsupportedEncodingException e) {
1852 throw new RuntimeException(e.toString());
1853 }
1854 }
1855
1856 private String decode(String s) {
1857 if (s == null) {
1858 return s;
1859 }
1860
1861 try {
1862 return URIEncoderDecoder.decode(s);
1863 } catch (UnsupportedEncodingException e) {
1864 throw new RuntimeException(e.toString());
1865 }
1866 }
1867
1868 /**
1869 * Returns the textual string representation of this URI instance using the
1870 * US-ASCII encoding.
1871 *
1872 * @return the US-ASCII string representation of this URI.
1873 */
1874 public String toASCIIString() {
1875 return encodeOthers(toString());
1876 }
1877
1878 /**
1879 * Returns the textual string representation of this URI instance.
1880 *
1881 * @return the textual string representation of this URI.
1882 */
1883 @Override
1884 public String toString() {
1885 return string;
1886 }
1887
1888 /*
1889 * Form a string from the components of this URI, similarly to the
1890 * toString() method. But this method converts scheme and host to lowercase,
1891 * and converts escaped octets to uppercase.
1892 *
1893 * Should convert octets to uppercase and follow platform specific
1894 * normalization rules for file: uri.
1895 */
1896 private String getHashString() {
1897 StringBuilder result = new StringBuilder();
1898 if (scheme != null) {
1899 result.append(toAsciiLowerCase(scheme));
1900 result.append(':');
1901 }
1902 if (opaque) {
1903 result.append(schemespecificpart);
1904 } else {
1905 if (authority != null) {
1906 result.append("//"); //$NON-NLS-1$
1907 if (host == null) {
1908 result.append(authority);
1909 } else {
1910 if (userinfo != null) {
1911 result.append(userinfo); //$NON-NLS-1$
1912 result.append("@");
1913 }
1914 result.append(toAsciiLowerCase(host));
1915 if (port != -1) {
1916 result.append(":"); //$NON-NLS-1$
1917 result.append(port);
1918 }
1919 }
1920 }
1921
1922 if (path != null) {
1923 if (fileSchemeCaseInsensitiveOS){
1924 result.append(toAsciiUpperCase(path));
1925 } else {
1926 result.append(path);
1927 }
1928 }
1929
1930 if (query != null) {
1931 result.append('?');
1932 result.append(query);
1933 }
1934 }
1935
1936 if (fragment != null) {
1937 result.append('#');
1938 result.append(fragment);
1939 }
1940
1941 return convertHexToUpperCase(result.toString());
1942 }
1943
1944 /**
1945 * Converts this URI instance to a URL.
1946 *
1947 * @return the created URL representing the same resource as this URI.
1948 * @throws MalformedURLException
1949 * if an error occurs while creating the URL or no protocol
1950 * handler could be found.
1951 */
1952 public URL toURL() throws MalformedURLException {
1953 if (!absolute) {
1954 throw new IllegalArgumentException(Messages.getString("luni.91") + ": " //$NON-NLS-1$//$NON-NLS-2$
1955 + toString());
1956 }
1957 return new URL(toString());
1958 }
1959 }