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