001 /* 002 * Copyright 2007-2015 UnboundID Corp. 003 * All Rights Reserved. 004 */ 005 /* 006 * Copyright (C) 2008-2015 UnboundID Corp. 007 * 008 * This program is free software; you can redistribute it and/or modify 009 * it under the terms of the GNU General Public License (GPLv2 only) 010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 011 * as published by the Free Software Foundation. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Public License for more details. 017 * 018 * You should have received a copy of the GNU General Public License 019 * along with this program; if not, see <http://www.gnu.org/licenses>. 020 */ 021 package com.unboundid.ldap.sdk; 022 023 024 025 import java.io.Serializable; 026 import java.nio.ByteBuffer; 027 import java.util.ArrayList; 028 029 030 import static com.unboundid.ldap.sdk.LDAPMessages.*; 031 import static com.unboundid.util.Debug.*; 032 import static com.unboundid.util.StaticUtils.*; 033 import static com.unboundid.util.Validator.*; 034 035 036 037 /** 038 * This class provides a data structure for interacting with LDAP URLs. It may 039 * be used to encode and decode URLs, as well as access the various elements 040 * that they contain. Note that this implementation currently does not support 041 * the use of extensions in an LDAP URL. 042 * <BR><BR> 043 * The components that may be included in an LDAP URL include: 044 * <UL> 045 * <LI>Scheme -- This specifies the protocol to use when communicating with 046 * the server. The official LDAP URL specification only allows a scheme 047 * of "{@code ldap}", but this implementation also supports the use of the 048 * "{@code ldaps}" scheme to indicate that clients should attempt to 049 * perform SSL-based communication with the target server (LDAPS) rather 050 * than unencrypted LDAP. It will also accept "{@code ldapi}", which is 051 * LDAP over UNIX domain sockets, although the LDAP SDK does not directly 052 * support that mechanism of communication.</LI> 053 * <LI>Host -- This specifies the address of the directory server to which the 054 * URL refers. If no host is provided, then it is expected that the 055 * client has some prior knowledge of the host (it often implies the same 056 * server from which the URL was retrieved).</LI> 057 * <LI>Port -- This specifies the port of the directory server to which the 058 * URL refers. If no host or port is provided, then it is assumed that 059 * the client has some prior knowledge of the instance to use (it often 060 * implies the same instance from which the URL was retrieved). If a host 061 * is provided without a port, then it should be assumed that the standard 062 * LDAP port of 389 should be used (or the standard LDAPS port of 636 if 063 * the scheme is "{@code ldaps}", or a value of 0 if the scheme is 064 * "{@code ldapi}").</LI> 065 * <LI>Base DN -- This specifies the base DN for the URL. If no base DN is 066 * provided, then a default of the null DN should be assumed.</LI> 067 * <LI>Requested attributes -- This specifies the set of requested attributes 068 * for the URL. If no attributes are specified, then the behavior should 069 * be the same as if no attributes had been provided for a search request 070 * (i.e., all user attributes should be included). 071 * <BR><BR> 072 * In the string representation of an LDAP URL, the names of the requested 073 * attributes (if more than one is provided) should be separated by 074 * commas.</LI> 075 * <LI>Scope -- This specifies the scope for the URL. It should be one of the 076 * standard scope values as defined in the {@code SearchRequest} 077 * class. If no scope is provided, then it should be assumed that a 078 * scope of {@code SearchScope#BASE} should be used. 079 * <BR><BR> 080 * In the string representation, the names of the scope values that are 081 * allowed include: 082 * <UL> 083 * <LI>base -- Equivalent to {@code SearchScope#BASE}.</LI> 084 * <LI>one -- Equivalent to {@code SearchScope#ONE}.</LI> 085 * <LI>sub -- Equivalent to {@code SearchScope#SUB}.</LI> 086 * <LI>subordinates -- Equivalent to 087 * {@code SearchScope#SUBORDINATE_SUBTREE}.</LI> 088 * </UL></LI> 089 * <LI>Filter -- This specifies the filter for the URL. If no filter is 090 * provided, then a default of "{@code (objectClass=*)}" should be 091 * assumed.</LI> 092 * </UL> 093 * An LDAP URL encapsulates many of the properties of a search request, and in 094 * fact the {@code LDAPURL#toSearchRequest} method may be used to create a 095 * {@code SearchRequest} object from an LDAP URL. 096 * <BR><BR> 097 * See <A HREF="http://www.ietf.org/rfc/rfc4516.txt">RFC 4516</A> for a complete 098 * description of the LDAP URL syntax. Some examples of LDAP URLs include: 099 * <UL> 100 * <LI>{@code ldap://} -- This is the smallest possible LDAP URL that can be 101 * represented. The default values will be used for all components other 102 * than the scheme.</LI> 103 * <LI>{@code 104 * ldap://server.example.com:1234/dc=example,dc=com?cn,sn?sub?(uid=john)} 105 * -- This is an example of a URL containing all of the elements. The 106 * scheme is "{@code ldap}", the host is "{@code server.example.com}", 107 * the port is "{@code 1234}", the base DN is "{@code dc=example,dc=com}", 108 * the requested attributes are "{@code cn}" and "{@code sn}", the scope 109 * is "{@code sub}" (which indicates a subtree scope equivalent to 110 * {@code SearchScope#SUB}), and a filter of 111 * "{@code (uid=john)}".</LI> 112 * </UL> 113 */ 114 public final class LDAPURL 115 implements Serializable 116 { 117 /** 118 * The default filter that will be used if none is provided. 119 */ 120 private static final Filter DEFAULT_FILTER = 121 Filter.createPresenceFilter("objectClass"); 122 123 124 125 /** 126 * The default port number that will be used for LDAP URLs if none is 127 * provided. 128 */ 129 public static final int DEFAULT_LDAP_PORT = 389; 130 131 132 133 /** 134 * The default port number that will be used for LDAPS URLs if none is 135 * provided. 136 */ 137 public static final int DEFAULT_LDAPS_PORT = 636; 138 139 140 141 /** 142 * The default port number that will be used for LDAPI URLs if none is 143 * provided. 144 */ 145 public static final int DEFAULT_LDAPI_PORT = 0; 146 147 148 149 /** 150 * The default scope that will be used if none is provided. 151 */ 152 private static final SearchScope DEFAULT_SCOPE = SearchScope.BASE; 153 154 155 156 /** 157 * The default base DN that will be used if none is provided. 158 */ 159 private static final DN DEFAULT_BASE_DN = DN.NULL_DN; 160 161 162 163 /** 164 * The default set of attributes that will be used if none is provided. 165 */ 166 private static final String[] DEFAULT_ATTRIBUTES = NO_STRINGS; 167 168 169 170 /** 171 * The serial version UID for this serializable class. 172 */ 173 private static final long serialVersionUID = 3420786933570240493L; 174 175 176 177 // Indicates whether the attribute list was provided in the URL. 178 private final boolean attributesProvided; 179 180 // Indicates whether the base DN was provided in the URL. 181 private final boolean baseDNProvided; 182 183 // Indicates whether the filter was provided in the URL. 184 private final boolean filterProvided; 185 186 // Indicates whether the port was provided in the URL. 187 private final boolean portProvided; 188 189 // Indicates whether the scope was provided in the URL. 190 private final boolean scopeProvided; 191 192 // The base DN used by this URL. 193 private final DN baseDN; 194 195 // The filter used by this URL. 196 private final Filter filter; 197 198 // The port used by this URL. 199 private final int port; 200 201 // The search scope used by this URL. 202 private final SearchScope scope; 203 204 // The host used by this URL. 205 private final String host; 206 207 // The normalized representation of this LDAP URL. 208 private volatile String normalizedURLString; 209 210 // The scheme used by this LDAP URL. The standard only accepts "ldap", but 211 // we will also accept "ldaps" and "ldapi". 212 private final String scheme; 213 214 // The string representation of this LDAP URL. 215 private final String urlString; 216 217 // The set of attributes included in this URL. 218 private final String[] attributes; 219 220 221 222 /** 223 * Creates a new LDAP URL from the provided string representation. 224 * 225 * @param urlString The string representation for this LDAP URL. It must 226 * not be {@code null}. 227 * 228 * @throws LDAPException If the provided URL string cannot be parsed as an 229 * LDAP URL. 230 */ 231 public LDAPURL(final String urlString) 232 throws LDAPException 233 { 234 ensureNotNull(urlString); 235 236 this.urlString = urlString; 237 238 239 // Find the location of the first colon. It should mark the end of the 240 // scheme. 241 final int colonPos = urlString.indexOf("://"); 242 if (colonPos < 0) 243 { 244 throw new LDAPException(ResultCode.DECODING_ERROR, 245 ERR_LDAPURL_NO_COLON_SLASHES.get()); 246 } 247 248 scheme = toLowerCase(urlString.substring(0, colonPos)); 249 final int defaultPort; 250 if (scheme.equals("ldap")) 251 { 252 defaultPort = DEFAULT_LDAP_PORT; 253 } 254 else if (scheme.equals("ldaps")) 255 { 256 defaultPort = DEFAULT_LDAPS_PORT; 257 } 258 else if (scheme.equals("ldapi")) 259 { 260 defaultPort = DEFAULT_LDAPI_PORT; 261 } 262 else 263 { 264 throw new LDAPException(ResultCode.DECODING_ERROR, 265 ERR_LDAPURL_INVALID_SCHEME.get(scheme)); 266 } 267 268 269 // Look for the first slash after the "://". It will designate the end of 270 // the hostport section. 271 final int slashPos = urlString.indexOf('/', colonPos+3); 272 if (slashPos < 0) 273 { 274 // This is fine. It just means that the URL won't have a base DN, 275 // attribute list, scope, or filter, and that the rest of the value is 276 // the hostport element. 277 baseDN = DEFAULT_BASE_DN; 278 baseDNProvided = false; 279 attributes = DEFAULT_ATTRIBUTES; 280 attributesProvided = false; 281 scope = DEFAULT_SCOPE; 282 scopeProvided = false; 283 filter = DEFAULT_FILTER; 284 filterProvided = false; 285 286 final String hostPort = urlString.substring(colonPos+3); 287 final StringBuilder hostBuffer = new StringBuilder(hostPort.length()); 288 final int portValue = decodeHostPort(hostPort, hostBuffer); 289 if (portValue < 0) 290 { 291 port = defaultPort; 292 portProvided = false; 293 } 294 else 295 { 296 port = portValue; 297 portProvided = true; 298 } 299 300 if (hostBuffer.length() == 0) 301 { 302 host = null; 303 } 304 else 305 { 306 host = hostBuffer.toString(); 307 } 308 return; 309 } 310 311 final String hostPort = urlString.substring(colonPos+3, slashPos); 312 final StringBuilder hostBuffer = new StringBuilder(hostPort.length()); 313 final int portValue = decodeHostPort(hostPort, hostBuffer); 314 if (portValue < 0) 315 { 316 port = defaultPort; 317 portProvided = false; 318 } 319 else 320 { 321 port = portValue; 322 portProvided = true; 323 } 324 325 if (hostBuffer.length() == 0) 326 { 327 host = null; 328 } 329 else 330 { 331 host = hostBuffer.toString(); 332 } 333 334 335 // Look for the first question mark after the slash. It will designate the 336 // end of the base DN. 337 final int questionMarkPos = urlString.indexOf('?', slashPos+1); 338 if (questionMarkPos < 0) 339 { 340 // This is fine. It just means that the URL won't have an attribute list, 341 // scope, or filter, and that the rest of the value is the base DN. 342 attributes = DEFAULT_ATTRIBUTES; 343 attributesProvided = false; 344 scope = DEFAULT_SCOPE; 345 scopeProvided = false; 346 filter = DEFAULT_FILTER; 347 filterProvided = false; 348 349 baseDN = new DN(percentDecode(urlString.substring(slashPos+1))); 350 baseDNProvided = (! baseDN.isNullDN()); 351 return; 352 } 353 354 baseDN = new DN(percentDecode(urlString.substring(slashPos+1, 355 questionMarkPos))); 356 baseDNProvided = (! baseDN.isNullDN()); 357 358 359 // Look for the next question mark. It will designate the end of the 360 // attribute list. 361 final int questionMark2Pos = urlString.indexOf('?', questionMarkPos+1); 362 if (questionMark2Pos < 0) 363 { 364 // This is fine. It just means that the URL won't have a scope or filter, 365 // and that the rest of the value is the attribute list. 366 scope = DEFAULT_SCOPE; 367 scopeProvided = false; 368 filter = DEFAULT_FILTER; 369 filterProvided = false; 370 371 attributes = decodeAttributes(urlString.substring(questionMarkPos+1)); 372 attributesProvided = (attributes.length > 0); 373 return; 374 } 375 376 attributes = decodeAttributes(urlString.substring(questionMarkPos+1, 377 questionMark2Pos)); 378 attributesProvided = (attributes.length > 0); 379 380 381 // Look for the next question mark. It will designate the end of the scope. 382 final int questionMark3Pos = urlString.indexOf('?', questionMark2Pos+1); 383 if (questionMark3Pos < 0) 384 { 385 // This is fine. It just means that the URL won't have a filter, and that 386 // the rest of the value is the scope. 387 filter = DEFAULT_FILTER; 388 filterProvided = false; 389 390 final String scopeStr = 391 toLowerCase(urlString.substring(questionMark2Pos+1)); 392 if (scopeStr.length() == 0) 393 { 394 scope = SearchScope.BASE; 395 scopeProvided = false; 396 } 397 else if (scopeStr.equals("base")) 398 { 399 scope = SearchScope.BASE; 400 scopeProvided = true; 401 } 402 else if (scopeStr.equals("one")) 403 { 404 scope = SearchScope.ONE; 405 scopeProvided = true; 406 } 407 else if (scopeStr.equals("sub")) 408 { 409 scope = SearchScope.SUB; 410 scopeProvided = true; 411 } 412 else if (scopeStr.equals("subord") || scopeStr.equals("subordinates")) 413 { 414 scope = SearchScope.SUBORDINATE_SUBTREE; 415 scopeProvided = true; 416 } 417 else 418 { 419 throw new LDAPException(ResultCode.DECODING_ERROR, 420 ERR_LDAPURL_INVALID_SCOPE.get(scopeStr)); 421 } 422 return; 423 } 424 425 final String scopeStr = 426 toLowerCase(urlString.substring(questionMark2Pos+1, questionMark3Pos)); 427 if (scopeStr.length() == 0) 428 { 429 scope = SearchScope.BASE; 430 scopeProvided = false; 431 } 432 else if (scopeStr.equals("base")) 433 { 434 scope = SearchScope.BASE; 435 scopeProvided = true; 436 } 437 else if (scopeStr.equals("one")) 438 { 439 scope = SearchScope.ONE; 440 scopeProvided = true; 441 } 442 else if (scopeStr.equals("sub")) 443 { 444 scope = SearchScope.SUB; 445 scopeProvided = true; 446 } 447 else if (scopeStr.equals("subord") || scopeStr.equals("subordinates")) 448 { 449 scope = SearchScope.SUBORDINATE_SUBTREE; 450 scopeProvided = true; 451 } 452 else 453 { 454 throw new LDAPException(ResultCode.DECODING_ERROR, 455 ERR_LDAPURL_INVALID_SCOPE.get(scopeStr)); 456 } 457 458 459 // The remainder of the value must be the filter. 460 final String filterStr = 461 percentDecode(urlString.substring(questionMark3Pos+1)); 462 if (filterStr.length() == 0) 463 { 464 filter = DEFAULT_FILTER; 465 filterProvided = false; 466 } 467 else 468 { 469 filter = Filter.create(filterStr); 470 filterProvided = true; 471 } 472 } 473 474 475 476 /** 477 * Creates a new LDAP URL with the provided information. 478 * 479 * @param scheme The scheme for this LDAP URL. It must not be 480 * {@code null} and must be either "ldap", "ldaps", or 481 * "ldapi". 482 * @param host The host for this LDAP URL. It may be {@code null} if 483 * no host is to be included. 484 * @param port The port for this LDAP URL. It may be {@code null} if 485 * no port is to be included. If it is provided, it must 486 * be between 1 and 65535, inclusive. 487 * @param baseDN The base DN for this LDAP URL. It may be {@code null} 488 * if no base DN is to be included. 489 * @param attributes The set of requested attributes for this LDAP URL. It 490 * may be {@code null} or empty if no attribute list is to 491 * be included. 492 * @param scope The scope for this LDAP URL. It may be {@code null} if 493 * no scope is to be included. Otherwise, it must be a 494 * value between zero and three, inclusive. 495 * @param filter The filter for this LDAP URL. It may be {@code null} 496 * if no filter is to be included. 497 * 498 * @throws LDAPException If there is a problem with any of the provided 499 * arguments. 500 */ 501 public LDAPURL(final String scheme, final String host, final Integer port, 502 final DN baseDN, final String[] attributes, 503 final SearchScope scope, final Filter filter) 504 throws LDAPException 505 { 506 ensureNotNull(scheme); 507 508 final StringBuilder buffer = new StringBuilder(); 509 510 this.scheme = toLowerCase(scheme); 511 final int defaultPort; 512 if (scheme.equals("ldap")) 513 { 514 defaultPort = DEFAULT_LDAP_PORT; 515 } 516 else if (scheme.equals("ldaps")) 517 { 518 defaultPort = DEFAULT_LDAPS_PORT; 519 } 520 else if (scheme.equals("ldapi")) 521 { 522 defaultPort = DEFAULT_LDAPI_PORT; 523 } 524 else 525 { 526 throw new LDAPException(ResultCode.DECODING_ERROR, 527 ERR_LDAPURL_INVALID_SCHEME.get(scheme)); 528 } 529 530 buffer.append(scheme); 531 buffer.append("://"); 532 533 if ((host == null) || (host.length() == 0)) 534 { 535 this.host = null; 536 } 537 else 538 { 539 this.host = host; 540 buffer.append(host); 541 } 542 543 if (port == null) 544 { 545 this.port = defaultPort; 546 portProvided = false; 547 } 548 else 549 { 550 this.port = port; 551 portProvided = true; 552 buffer.append(':'); 553 buffer.append(port); 554 555 if ((port < 1) || (port > 65535)) 556 { 557 throw new LDAPException(ResultCode.PARAM_ERROR, 558 ERR_LDAPURL_INVALID_PORT.get(port)); 559 } 560 } 561 562 buffer.append('/'); 563 if (baseDN == null) 564 { 565 this.baseDN = DEFAULT_BASE_DN; 566 baseDNProvided = false; 567 } 568 else 569 { 570 this.baseDN = baseDN; 571 baseDNProvided = true; 572 percentEncode(baseDN.toString(), buffer); 573 } 574 575 final boolean continueAppending; 576 if (((attributes == null) || (attributes.length == 0)) && (scope == null) && 577 (filter == null)) 578 { 579 continueAppending = false; 580 } 581 else 582 { 583 continueAppending = true; 584 } 585 586 if (continueAppending) 587 { 588 buffer.append('?'); 589 } 590 if ((attributes == null) || (attributes.length == 0)) 591 { 592 this.attributes = DEFAULT_ATTRIBUTES; 593 attributesProvided = false; 594 } 595 else 596 { 597 this.attributes = attributes; 598 attributesProvided = true; 599 600 for (int i=0; i < attributes.length; i++) 601 { 602 if (i > 0) 603 { 604 buffer.append(','); 605 } 606 buffer.append(attributes[i]); 607 } 608 } 609 610 if (continueAppending) 611 { 612 buffer.append('?'); 613 } 614 if (scope == null) 615 { 616 this.scope = DEFAULT_SCOPE; 617 scopeProvided = false; 618 } 619 else 620 { 621 switch (scope.intValue()) 622 { 623 case 0: 624 this.scope = scope; 625 scopeProvided = true; 626 buffer.append("base"); 627 break; 628 case 1: 629 this.scope = scope; 630 scopeProvided = true; 631 buffer.append("one"); 632 break; 633 case 2: 634 this.scope = scope; 635 scopeProvided = true; 636 buffer.append("sub"); 637 break; 638 case 3: 639 this.scope = scope; 640 scopeProvided = true; 641 buffer.append("subordinates"); 642 break; 643 default: 644 throw new LDAPException(ResultCode.PARAM_ERROR, 645 ERR_LDAPURL_INVALID_SCOPE_VALUE.get(scope)); 646 } 647 } 648 649 if (continueAppending) 650 { 651 buffer.append('?'); 652 } 653 if (filter == null) 654 { 655 this.filter = DEFAULT_FILTER; 656 filterProvided = false; 657 } 658 else 659 { 660 this.filter = filter; 661 filterProvided = true; 662 percentEncode(filter.toString(), buffer); 663 } 664 665 urlString = buffer.toString(); 666 } 667 668 669 670 /** 671 * Decodes the provided string as a host and optional port number. 672 * 673 * @param hostPort The string to be decoded. 674 * @param hostBuffer The buffer to which the decoded host address will be 675 * appended. 676 * 677 * @return The port number decoded from the provided string, or -1 if there 678 * was no port number. 679 * 680 * @throws LDAPException If the provided string cannot be decoded as a 681 * hostport element. 682 */ 683 private static int decodeHostPort(final String hostPort, 684 final StringBuilder hostBuffer) 685 throws LDAPException 686 { 687 final int length = hostPort.length(); 688 if (length == 0) 689 { 690 // It's an empty string, so we'll just use the defaults. 691 return -1; 692 } 693 694 if (hostPort.charAt(0) == '[') 695 { 696 // It starts with a square bracket, which means that the address is an 697 // IPv6 literal address. Find the closing bracket, and the address 698 // will be inside them. 699 final int closingBracketPos = hostPort.indexOf(']'); 700 if (closingBracketPos < 0) 701 { 702 throw new LDAPException(ResultCode.DECODING_ERROR, 703 ERR_LDAPURL_IPV6_HOST_MISSING_BRACKET.get()); 704 } 705 706 hostBuffer.append(hostPort.substring(1, closingBracketPos).trim()); 707 if (hostBuffer.length() == 0) 708 { 709 throw new LDAPException(ResultCode.DECODING_ERROR, 710 ERR_LDAPURL_IPV6_HOST_EMPTY.get()); 711 } 712 713 // The closing bracket must either be the end of the hostport element 714 // (in which case we'll use the default port), or it must be followed by 715 // a colon and an integer (which will be the port). 716 if (closingBracketPos == (length - 1)) 717 { 718 return -1; 719 } 720 else 721 { 722 if (hostPort.charAt(closingBracketPos+1) != ':') 723 { 724 throw new LDAPException(ResultCode.DECODING_ERROR, 725 ERR_LDAPURL_IPV6_HOST_UNEXPECTED_CHAR.get( 726 hostPort.charAt(closingBracketPos+1))); 727 } 728 else 729 { 730 try 731 { 732 final int decodedPort = 733 Integer.parseInt(hostPort.substring(closingBracketPos+2)); 734 if ((decodedPort >= 1) && (decodedPort <= 65535)) 735 { 736 return decodedPort; 737 } 738 else 739 { 740 throw new LDAPException(ResultCode.DECODING_ERROR, 741 ERR_LDAPURL_INVALID_PORT.get( 742 decodedPort)); 743 } 744 } 745 catch (NumberFormatException nfe) 746 { 747 debugException(nfe); 748 throw new LDAPException(ResultCode.DECODING_ERROR, 749 ERR_LDAPURL_PORT_NOT_INT.get(hostPort), 750 nfe); 751 } 752 } 753 } 754 } 755 756 757 // If we've gotten here, then the address is either a resolvable name or an 758 // IPv4 address. If there is a colon in the string, then it will separate 759 // the address from the port. Otherwise, the remaining value will be the 760 // address and we'll use the default port. 761 final int colonPos = hostPort.indexOf(':'); 762 if (colonPos < 0) 763 { 764 hostBuffer.append(hostPort); 765 return -1; 766 } 767 else 768 { 769 try 770 { 771 final int decodedPort = 772 Integer.parseInt(hostPort.substring(colonPos+1)); 773 if ((decodedPort >= 1) && (decodedPort <= 65535)) 774 { 775 hostBuffer.append(hostPort.substring(0, colonPos)); 776 return decodedPort; 777 } 778 else 779 { 780 throw new LDAPException(ResultCode.DECODING_ERROR, 781 ERR_LDAPURL_INVALID_PORT.get(decodedPort)); 782 } 783 } 784 catch (NumberFormatException nfe) 785 { 786 debugException(nfe); 787 throw new LDAPException(ResultCode.DECODING_ERROR, 788 ERR_LDAPURL_PORT_NOT_INT.get(hostPort), nfe); 789 } 790 } 791 } 792 793 794 795 /** 796 * Decodes the contents of the provided string as an attribute list. 797 * 798 * @param s The string to decode as an attribute list. 799 * 800 * @return The array of decoded attribute names. 801 * 802 * @throws LDAPException If an error occurred while attempting to decode the 803 * attribute list. 804 */ 805 private static String[] decodeAttributes(final String s) 806 throws LDAPException 807 { 808 final int length = s.length(); 809 if (length == 0) 810 { 811 return DEFAULT_ATTRIBUTES; 812 } 813 814 final ArrayList<String> attrList = new ArrayList<String>(); 815 int startPos = 0; 816 while (startPos < length) 817 { 818 final int commaPos = s.indexOf(',', startPos); 819 if (commaPos < 0) 820 { 821 // There are no more commas, so there can only be one attribute left. 822 final String attrName = s.substring(startPos).trim(); 823 if (attrName.length() == 0) 824 { 825 // This is only acceptable if the attribute list is empty (there was 826 // probably a space in the attribute list string, which is technically 827 // not allowed, but we'll accept it). If the attribute list is not 828 // empty, then there were two consecutive commas, which is not 829 // allowed. 830 if (attrList.isEmpty()) 831 { 832 return DEFAULT_ATTRIBUTES; 833 } 834 else 835 { 836 throw new LDAPException(ResultCode.DECODING_ERROR, 837 ERR_LDAPURL_ATTRLIST_ENDS_WITH_COMMA.get()); 838 } 839 } 840 else 841 { 842 attrList.add(attrName); 843 break; 844 } 845 } 846 else 847 { 848 final String attrName = s.substring(startPos, commaPos).trim(); 849 if (attrName.length() == 0) 850 { 851 throw new LDAPException(ResultCode.DECODING_ERROR, 852 ERR_LDAPURL_ATTRLIST_EMPTY_ATTRIBUTE.get()); 853 } 854 else 855 { 856 attrList.add(attrName); 857 startPos = commaPos+1; 858 if (startPos >= length) 859 { 860 throw new LDAPException(ResultCode.DECODING_ERROR, 861 ERR_LDAPURL_ATTRLIST_ENDS_WITH_COMMA.get()); 862 } 863 } 864 } 865 } 866 867 final String[] attributes = new String[attrList.size()]; 868 attrList.toArray(attributes); 869 return attributes; 870 } 871 872 873 874 /** 875 * Decodes any percent-encoded values that may be contained in the provided 876 * string. 877 * 878 * @param s The string to be decoded. 879 * 880 * @return The percent-decoded form of the provided string. 881 * 882 * @throws LDAPException If a problem occurs while attempting to decode the 883 * provided string. 884 */ 885 public static String percentDecode(final String s) 886 throws LDAPException 887 { 888 // First, see if there are any percent characters at all in the provided 889 // string. If not, then just return the string as-is. 890 int firstPercentPos = -1; 891 final int length = s.length(); 892 for (int i=0; i < length; i++) 893 { 894 if (s.charAt(i) == '%') 895 { 896 firstPercentPos = i; 897 break; 898 } 899 } 900 901 if (firstPercentPos < 0) 902 { 903 return s; 904 } 905 906 int pos = firstPercentPos; 907 final StringBuilder buffer = new StringBuilder(2 * length); 908 buffer.append(s.substring(0, firstPercentPos)); 909 910 while (pos < length) 911 { 912 final char c = s.charAt(pos++); 913 if (c == '%') 914 { 915 if (pos >= length) 916 { 917 throw new LDAPException(ResultCode.DECODING_ERROR, 918 ERR_LDAPURL_HEX_STRING_TOO_SHORT.get(s)); 919 } 920 921 922 final ByteBuffer byteBuffer = ByteBuffer.allocate(length - pos); 923 while (pos < length) 924 { 925 byte b; 926 switch (s.charAt(pos++)) 927 { 928 case '0': 929 b = 0x00; 930 break; 931 case '1': 932 b = 0x10; 933 break; 934 case '2': 935 b = 0x20; 936 break; 937 case '3': 938 b = 0x30; 939 break; 940 case '4': 941 b = 0x40; 942 break; 943 case '5': 944 b = 0x50; 945 break; 946 case '6': 947 b = 0x60; 948 break; 949 case '7': 950 b = 0x70; 951 break; 952 case '8': 953 b = (byte) 0x80; 954 break; 955 case '9': 956 b = (byte) 0x90; 957 break; 958 case 'a': 959 case 'A': 960 b = (byte) 0xA0; 961 break; 962 case 'b': 963 case 'B': 964 b = (byte) 0xB0; 965 break; 966 case 'c': 967 case 'C': 968 b = (byte) 0xC0; 969 break; 970 case 'd': 971 case 'D': 972 b = (byte) 0xD0; 973 break; 974 case 'e': 975 case 'E': 976 b = (byte) 0xE0; 977 break; 978 case 'f': 979 case 'F': 980 b = (byte) 0xF0; 981 break; 982 default: 983 throw new LDAPException(ResultCode.DECODING_ERROR, 984 ERR_LDAPURL_INVALID_HEX_CHAR.get( 985 s.charAt(pos-1))); 986 } 987 988 if (pos >= length) 989 { 990 throw new LDAPException(ResultCode.DECODING_ERROR, 991 ERR_LDAPURL_HEX_STRING_TOO_SHORT.get(s)); 992 } 993 994 switch (s.charAt(pos++)) 995 { 996 case '0': 997 b |= 0x00; 998 break; 999 case '1': 1000 b |= 0x01; 1001 break; 1002 case '2': 1003 b |= 0x02; 1004 break; 1005 case '3': 1006 b |= 0x03; 1007 break; 1008 case '4': 1009 b |= 0x04; 1010 break; 1011 case '5': 1012 b |= 0x05; 1013 break; 1014 case '6': 1015 b |= 0x06; 1016 break; 1017 case '7': 1018 b |= 0x07; 1019 break; 1020 case '8': 1021 b |= 0x08; 1022 break; 1023 case '9': 1024 b |= 0x09; 1025 break; 1026 case 'a': 1027 case 'A': 1028 b |= 0x0A; 1029 break; 1030 case 'b': 1031 case 'B': 1032 b |= 0x0B; 1033 break; 1034 case 'c': 1035 case 'C': 1036 b |= 0x0C; 1037 break; 1038 case 'd': 1039 case 'D': 1040 b |= 0x0D; 1041 break; 1042 case 'e': 1043 case 'E': 1044 b |= 0x0E; 1045 break; 1046 case 'f': 1047 case 'F': 1048 b |= 0x0F; 1049 break; 1050 default: 1051 throw new LDAPException(ResultCode.DECODING_ERROR, 1052 ERR_LDAPURL_INVALID_HEX_CHAR.get( 1053 s.charAt(pos-1))); 1054 } 1055 1056 byteBuffer.put(b); 1057 if ((pos < length) && (s.charAt(pos) != '%')) 1058 { 1059 break; 1060 } 1061 } 1062 1063 byteBuffer.flip(); 1064 final byte[] byteArray = new byte[byteBuffer.limit()]; 1065 byteBuffer.get(byteArray); 1066 1067 buffer.append(toUTF8String(byteArray)); 1068 } 1069 else 1070 { 1071 buffer.append(c); 1072 } 1073 } 1074 1075 return buffer.toString(); 1076 } 1077 1078 1079 1080 /** 1081 * Appends an encoded version of the provided string to the given buffer. Any 1082 * special characters contained in the string will be replaced with byte 1083 * representations consisting of one percent sign and two hexadecimal digits 1084 * for each byte in the special character. 1085 * 1086 * @param s The string to be encoded. 1087 * @param buffer The buffer to which the encoded string will be written. 1088 */ 1089 private static void percentEncode(final String s, final StringBuilder buffer) 1090 { 1091 final int length = s.length(); 1092 for (int i=0; i < length; i++) 1093 { 1094 final char c = s.charAt(i); 1095 1096 switch (c) 1097 { 1098 case 'A': 1099 case 'B': 1100 case 'C': 1101 case 'D': 1102 case 'E': 1103 case 'F': 1104 case 'G': 1105 case 'H': 1106 case 'I': 1107 case 'J': 1108 case 'K': 1109 case 'L': 1110 case 'M': 1111 case 'N': 1112 case 'O': 1113 case 'P': 1114 case 'Q': 1115 case 'R': 1116 case 'S': 1117 case 'T': 1118 case 'U': 1119 case 'V': 1120 case 'W': 1121 case 'X': 1122 case 'Y': 1123 case 'Z': 1124 case 'a': 1125 case 'b': 1126 case 'c': 1127 case 'd': 1128 case 'e': 1129 case 'f': 1130 case 'g': 1131 case 'h': 1132 case 'i': 1133 case 'j': 1134 case 'k': 1135 case 'l': 1136 case 'm': 1137 case 'n': 1138 case 'o': 1139 case 'p': 1140 case 'q': 1141 case 'r': 1142 case 's': 1143 case 't': 1144 case 'u': 1145 case 'v': 1146 case 'w': 1147 case 'x': 1148 case 'y': 1149 case 'z': 1150 case '0': 1151 case '1': 1152 case '2': 1153 case '3': 1154 case '4': 1155 case '5': 1156 case '6': 1157 case '7': 1158 case '8': 1159 case '9': 1160 case '-': 1161 case '.': 1162 case '_': 1163 case '~': 1164 case '!': 1165 case '$': 1166 case '&': 1167 case '\'': 1168 case '(': 1169 case ')': 1170 case '*': 1171 case '+': 1172 case ',': 1173 case ';': 1174 case '=': 1175 buffer.append(c); 1176 break; 1177 1178 default: 1179 final byte[] charBytes = getBytes(new String(new char[] { c })); 1180 for (final byte b : charBytes) 1181 { 1182 buffer.append('%'); 1183 toHex(b, buffer); 1184 } 1185 break; 1186 } 1187 } 1188 } 1189 1190 1191 1192 /** 1193 * Retrieves the scheme for this LDAP URL. It will either be "ldap", "ldaps", 1194 * or "ldapi". 1195 * 1196 * @return The scheme for this LDAP URL. 1197 */ 1198 public String getScheme() 1199 { 1200 return scheme; 1201 } 1202 1203 1204 1205 /** 1206 * Retrieves the host for this LDAP URL. 1207 * 1208 * @return The host for this LDAP URL, or {@code null} if the URL does not 1209 * include a host and the client is supposed to have some external 1210 * knowledge of what the host should be. 1211 */ 1212 public String getHost() 1213 { 1214 return host; 1215 } 1216 1217 1218 1219 /** 1220 * Indicates whether the URL explicitly included a host address. 1221 * 1222 * @return {@code true} if the URL explicitly included a host address, or 1223 * {@code false} if it did not. 1224 */ 1225 public boolean hostProvided() 1226 { 1227 return (host != null); 1228 } 1229 1230 1231 1232 /** 1233 * Retrieves the port for this LDAP URL. 1234 * 1235 * @return The port for this LDAP URL. 1236 */ 1237 public int getPort() 1238 { 1239 return port; 1240 } 1241 1242 1243 1244 /** 1245 * Indicates whether the URL explicitly included a port number. 1246 * 1247 * @return {@code true} if the URL explicitly included a port number, or 1248 * {@code false} if it did not and the default should be used. 1249 */ 1250 public boolean portProvided() 1251 { 1252 return portProvided; 1253 } 1254 1255 1256 1257 /** 1258 * Retrieves the base DN for this LDAP URL. 1259 * 1260 * @return The base DN for this LDAP URL. 1261 */ 1262 public DN getBaseDN() 1263 { 1264 return baseDN; 1265 } 1266 1267 1268 1269 /** 1270 * Indicates whether the URL explicitly included a base DN. 1271 * 1272 * @return {@code true} if the URL explicitly included a base DN, or 1273 * {@code false} if it did not and the default should be used. 1274 */ 1275 public boolean baseDNProvided() 1276 { 1277 return baseDNProvided; 1278 } 1279 1280 1281 1282 /** 1283 * Retrieves the attribute list for this LDAP URL. 1284 * 1285 * @return The attribute list for this LDAP URL. 1286 */ 1287 public String[] getAttributes() 1288 { 1289 return attributes; 1290 } 1291 1292 1293 1294 /** 1295 * Indicates whether the URL explicitly included an attribute list. 1296 * 1297 * @return {@code true} if the URL explicitly included an attribute list, or 1298 * {@code false} if it did not and the default should be used. 1299 */ 1300 public boolean attributesProvided() 1301 { 1302 return attributesProvided; 1303 } 1304 1305 1306 1307 /** 1308 * Retrieves the scope for this LDAP URL. 1309 * 1310 * @return The scope for this LDAP URL. 1311 */ 1312 public SearchScope getScope() 1313 { 1314 return scope; 1315 } 1316 1317 1318 1319 /** 1320 * Indicates whether the URL explicitly included a search scope. 1321 * 1322 * @return {@code true} if the URL explicitly included a search scope, or 1323 * {@code false} if it did not and the default should be used. 1324 */ 1325 public boolean scopeProvided() 1326 { 1327 return scopeProvided; 1328 } 1329 1330 1331 1332 /** 1333 * Retrieves the filter for this LDAP URL. 1334 * 1335 * @return The filter for this LDAP URL. 1336 */ 1337 public Filter getFilter() 1338 { 1339 return filter; 1340 } 1341 1342 1343 1344 /** 1345 * Indicates whether the URL explicitly included a search filter. 1346 * 1347 * @return {@code true} if the URL explicitly included a search filter, or 1348 * {@code false} if it did not and the default should be used. 1349 */ 1350 public boolean filterProvided() 1351 { 1352 return filterProvided; 1353 } 1354 1355 1356 1357 /** 1358 * Creates a search request containing the base DN, scope, filter, and 1359 * requested attributes from this LDAP URL. 1360 * 1361 * @return The search request created from the base DN, scope, filter, and 1362 * requested attributes from this LDAP URL. 1363 */ 1364 public SearchRequest toSearchRequest() 1365 { 1366 return new SearchRequest(baseDN.toString(), scope, filter, attributes); 1367 } 1368 1369 1370 1371 /** 1372 * Retrieves a hash code for this LDAP URL. 1373 * 1374 * @return A hash code for this LDAP URL. 1375 */ 1376 @Override() 1377 public int hashCode() 1378 { 1379 return toNormalizedString().hashCode(); 1380 } 1381 1382 1383 1384 /** 1385 * Indicates whether the provided object is equal to this LDAP URL. In order 1386 * to be considered equal, the provided object must be an LDAP URL with the 1387 * same normalized string representation. 1388 * 1389 * @param o The object for which to make the determination. 1390 * 1391 * @return {@code true} if the provided object is equal to this LDAP URL, or 1392 * {@code false} if not. 1393 */ 1394 @Override() 1395 public boolean equals(final Object o) 1396 { 1397 if (o == null) 1398 { 1399 return false; 1400 } 1401 1402 if (o == this) 1403 { 1404 return true; 1405 } 1406 1407 if (! (o instanceof LDAPURL)) 1408 { 1409 return false; 1410 } 1411 1412 final LDAPURL url = (LDAPURL) o; 1413 return toNormalizedString().equals(url.toNormalizedString()); 1414 } 1415 1416 1417 1418 /** 1419 * Retrieves a string representation of this LDAP URL. 1420 * 1421 * @return A string representation of this LDAP URL. 1422 */ 1423 @Override() 1424 public String toString() 1425 { 1426 return urlString; 1427 } 1428 1429 1430 1431 /** 1432 * Retrieves a normalized string representation of this LDAP URL. 1433 * 1434 * @return A normalized string representation of this LDAP URL. 1435 */ 1436 public String toNormalizedString() 1437 { 1438 if (normalizedURLString == null) 1439 { 1440 final StringBuilder buffer = new StringBuilder(); 1441 toNormalizedString(buffer); 1442 normalizedURLString = buffer.toString(); 1443 } 1444 1445 return normalizedURLString; 1446 } 1447 1448 1449 1450 /** 1451 * Appends a normalized string representation of this LDAP URL to the provided 1452 * buffer. 1453 * 1454 * @param buffer The buffer to which to append the normalized string 1455 * representation of this LDAP URL. 1456 */ 1457 public void toNormalizedString(final StringBuilder buffer) 1458 { 1459 buffer.append(scheme); 1460 buffer.append("://"); 1461 1462 if (host != null) 1463 { 1464 if (host.indexOf(':') >= 0) 1465 { 1466 buffer.append('['); 1467 buffer.append(toLowerCase(host)); 1468 buffer.append(']'); 1469 } 1470 else 1471 { 1472 buffer.append(toLowerCase(host)); 1473 } 1474 } 1475 1476 if (! scheme.equals("ldapi")) 1477 { 1478 buffer.append(':'); 1479 buffer.append(port); 1480 } 1481 1482 buffer.append('/'); 1483 percentEncode(baseDN.toNormalizedString(), buffer); 1484 buffer.append('?'); 1485 1486 for (int i=0; i < attributes.length; i++) 1487 { 1488 if (i > 0) 1489 { 1490 buffer.append(','); 1491 } 1492 1493 buffer.append(toLowerCase(attributes[i])); 1494 } 1495 1496 buffer.append('?'); 1497 switch (scope.intValue()) 1498 { 1499 case 0: // BASE 1500 buffer.append("base"); 1501 break; 1502 case 1: // ONE 1503 buffer.append("one"); 1504 break; 1505 case 2: // SUB 1506 buffer.append("sub"); 1507 break; 1508 case 3: // SUBORDINATE_SUBTREE 1509 buffer.append("subordinates"); 1510 break; 1511 } 1512 1513 buffer.append('?'); 1514 percentEncode(filter.toNormalizedString(), buffer); 1515 } 1516 }