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