001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions Copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2006-2008 Sun Microsystems, Inc. 015 * Portions Copyright 2012-2016 ForgeRock AS. 016 */ 017package org.opends.server.types; 018 019import java.util.Iterator; 020import java.util.LinkedHashSet; 021import java.util.LinkedList; 022import java.util.List; 023import java.util.Objects; 024import java.util.Set; 025import java.util.StringTokenizer; 026 027import org.forgerock.i18n.LocalizableMessage; 028import org.forgerock.i18n.slf4j.LocalizedLogger; 029import org.forgerock.opendj.ldap.DN; 030import org.forgerock.opendj.ldap.ResultCode; 031import org.forgerock.opendj.ldap.SearchScope; 032import org.forgerock.opendj.ldap.schema.CoreSchema; 033 034import static org.forgerock.opendj.ldap.ResultCode.*; 035import static org.opends.messages.UtilityMessages.*; 036import static org.opends.server.util.StaticUtils.*; 037 038/** 039 * This class defines a data structure that represents the components 040 * of an LDAP URL, including the scheme, host, port, base DN, 041 * attributes, scope, filter, and extensions. It has the ability to 042 * create an LDAP URL based on all of these individual components, as 043 * well as parsing them from their string representations. 044 */ 045@org.opends.server.types.PublicAPI( 046 stability=org.opends.server.types.StabilityLevel.UNCOMMITTED, 047 mayInstantiate=true, 048 mayExtend=false, 049 mayInvoke=true) 050public final class LDAPURL 051{ 052 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 053 054 /** The default scheme that will be used if none is provided. */ 055 public static final String DEFAULT_SCHEME = "ldap"; 056 /** The default port value that will be used if none is provided. */ 057 private static final int DEFAULT_PORT = 389; 058 /** The default base DN that will be used if none is provided. */ 059 private static final DN DEFAULT_BASE_DN = DN.rootDN(); 060 /** The default search scope that will be used if none is provided. */ 061 private static final SearchScope DEFAULT_SEARCH_SCOPE = 062 SearchScope.BASE_OBJECT; 063 /** The default search filter that will be used if none is provided. */ 064 public static final SearchFilter DEFAULT_SEARCH_FILTER = 065 SearchFilter.createPresenceFilter(CoreSchema.getObjectClassAttributeType()); 066 067 068 /** The host for this LDAP URL. */ 069 private String host; 070 /** The port number for this LDAP URL. */ 071 private int port; 072 /** The base DN for this LDAP URL. */ 073 private DN baseDN; 074 /** The raw base DN for this LDAP URL. */ 075 private String rawBaseDN; 076 /** The search scope for this LDAP URL. */ 077 private SearchScope scope; 078 /** The search filter for this LDAP URL. */ 079 private SearchFilter filter; 080 /** The raw filter for this LDAP URL. */ 081 private String rawFilter; 082 083 /** The set of attributes for this LDAP URL. */ 084 private final LinkedHashSet<String> attributes; 085 /** The set of extensions for this LDAP URL. */ 086 private final List<String> extensions; 087 088 089 /** The scheme (i.e., protocol) for this LDAP URL. */ 090 private String scheme; 091 092 093 094 /** 095 * Creates a new LDAP URL with the provided information. 096 * 097 * @param scheme The scheme (i.e., protocol) for this LDAP 098 * URL. 099 * @param host The address for this LDAP URL. 100 * @param port The port number for this LDAP URL. 101 * @param rawBaseDN The raw base DN for this LDAP URL. 102 * @param attributes The set of requested attributes for this LDAP 103 * URL. 104 * @param scope The search scope for this LDAP URL. 105 * @param rawFilter The string representation of the search 106 * filter for this LDAP URL. 107 * @param extensions The set of extensions for this LDAP URL. 108 */ 109 public LDAPURL(String scheme, String host, int port, 110 String rawBaseDN, LinkedHashSet<String> attributes, 111 SearchScope scope, String rawFilter, 112 List<String> extensions) 113 { 114 this.host = toLowerCase(host); 115 116 baseDN = null; 117 filter = null; 118 119 120 if (scheme == null) 121 { 122 this.scheme = "ldap"; 123 } 124 else 125 { 126 this.scheme = toLowerCase(scheme); 127 } 128 129 this.port = toPort(port); 130 131 if (rawBaseDN == null) 132 { 133 this.rawBaseDN = ""; 134 } 135 else 136 { 137 this.rawBaseDN = rawBaseDN; 138 } 139 140 if (attributes == null) 141 { 142 this.attributes = new LinkedHashSet<>(); 143 } 144 else 145 { 146 this.attributes = attributes; 147 } 148 149 if (scope == null) 150 { 151 this.scope = DEFAULT_SEARCH_SCOPE; 152 } 153 else 154 { 155 this.scope = scope; 156 } 157 158 if (rawFilter != null) 159 { 160 this.rawFilter = rawFilter; 161 } 162 else 163 { 164 setFilter(SearchFilter.objectClassPresent()); 165 } 166 167 if (extensions == null) 168 { 169 this.extensions = new LinkedList<>(); 170 } 171 else 172 { 173 this.extensions = extensions; 174 } 175 } 176 177 178 179 /** 180 * Creates a new LDAP URL with the provided information. 181 * 182 * @param scheme The scheme (i.e., protocol) for this LDAP 183 * URL. 184 * @param host The address for this LDAP URL. 185 * @param port The port number for this LDAP URL. 186 * @param baseDN The base DN for this LDAP URL. 187 * @param attributes The set of requested attributes for this LDAP 188 * URL. 189 * @param scope The search scope for this LDAP URL. 190 * @param filter The search filter for this LDAP URL. 191 * @param extensions The set of extensions for this LDAP URL. 192 */ 193 public LDAPURL(String scheme, String host, int port, DN baseDN, 194 LinkedHashSet<String> attributes, SearchScope scope, 195 SearchFilter filter, List<String> extensions) 196 { 197 this.host = toLowerCase(host); 198 199 200 if (scheme == null) 201 { 202 this.scheme = "ldap"; 203 } 204 else 205 { 206 this.scheme = toLowerCase(scheme); 207 } 208 209 this.port = toPort(port); 210 211 if (baseDN == null) 212 { 213 this.baseDN = DEFAULT_BASE_DN; 214 this.rawBaseDN = DEFAULT_BASE_DN.toString(); 215 } 216 else 217 { 218 this.baseDN = baseDN; 219 this.rawBaseDN = baseDN.toString(); 220 } 221 222 if (attributes == null) 223 { 224 this.attributes = new LinkedHashSet<>(); 225 } 226 else 227 { 228 this.attributes = attributes; 229 } 230 231 if (scope == null) 232 { 233 this.scope = DEFAULT_SEARCH_SCOPE; 234 } 235 else 236 { 237 this.scope = scope; 238 } 239 240 if (filter == null) 241 { 242 this.filter = DEFAULT_SEARCH_FILTER; 243 this.rawFilter = DEFAULT_SEARCH_FILTER.toString(); 244 } 245 else 246 { 247 this.filter = filter; 248 this.rawFilter = filter.toString(); 249 } 250 251 if (extensions == null) 252 { 253 this.extensions = new LinkedList<>(); 254 } 255 else 256 { 257 this.extensions = extensions; 258 } 259 } 260 261 262 263 /** 264 * Decodes the provided string as an LDAP URL. 265 * 266 * @param url The URL string to be decoded. 267 * @param fullyDecode Indicates whether the URL should be fully 268 * decoded (e.g., parsing the base DN and 269 * search filter) or just leaving them in their 270 * string representations. The latter may be 271 * required for client-side use. 272 * 273 * @return The LDAP URL decoded from the provided string. 274 * 275 * @throws DirectoryException If a problem occurs while attempting 276 * to decode the provided string as an 277 * LDAP URL. 278 */ 279 public static LDAPURL decode(String url, boolean fullyDecode) 280 throws DirectoryException 281 { 282 // Find the "://" component, which will separate the scheme from 283 // the host. 284 String scheme; 285 int schemeEndPos = url.indexOf("://"); 286 if (schemeEndPos < 0) 287 { 288 LocalizableMessage message = ERR_LDAPURL_NO_COLON_SLASH_SLASH.get(url); 289 throw new DirectoryException(INVALID_ATTRIBUTE_SYNTAX, message); 290 } 291 else if (schemeEndPos == 0) 292 { 293 LocalizableMessage message = ERR_LDAPURL_NO_SCHEME.get(url); 294 throw new DirectoryException(INVALID_ATTRIBUTE_SYNTAX, message); 295 } 296 else 297 { 298 scheme = urlDecode(url.substring(0, schemeEndPos)); 299 // FIXME also need to check that the scheme is actually ldap/ldaps!! 300 } 301 302 303 // If the "://" was the end of the URL, then we're done. 304 int length = url.length(); 305 if (length == schemeEndPos+3) 306 { 307 return new LDAPURL(scheme, null, DEFAULT_PORT, DEFAULT_BASE_DN, 308 null, DEFAULT_SEARCH_SCOPE, 309 DEFAULT_SEARCH_FILTER, null); 310 } 311 312 313 // Look at the next character. If it's anything but a slash, then 314 // it should be part of the host and optional port. 315 String host = null; 316 int port = DEFAULT_PORT; 317 int startPos = schemeEndPos + 3; 318 int pos = startPos; 319 while (pos < length) 320 { 321 char c = url.charAt(pos); 322 if (c == '/') 323 { 324 break; 325 } 326 pos++; 327 } 328 329 if (pos > startPos) 330 { 331 String hostPort = url.substring(startPos, pos); 332 int colonPos = hostPort.lastIndexOf(':'); 333 if (colonPos < 0) 334 { 335 host = urlDecode(hostPort); 336 } 337 else if (colonPos == 0) 338 { 339 LocalizableMessage message = ERR_LDAPURL_NO_HOST.get(url); 340 throw new DirectoryException(INVALID_ATTRIBUTE_SYNTAX, message); 341 } 342 else if (colonPos == (hostPort.length() - 1)) 343 { 344 LocalizableMessage message = ERR_LDAPURL_NO_PORT.get(url); 345 throw new DirectoryException(INVALID_ATTRIBUTE_SYNTAX, message); 346 } 347 else 348 { 349 try 350 { 351 final HostPort hp = HostPort.valueOf(hostPort); 352 host = urlDecode(hp.getHost()); 353 port = hp.getPort(); 354 } 355 catch (NumberFormatException e) 356 { 357 LocalizableMessage message = ERR_LDAPURL_CANNOT_DECODE_PORT.get( 358 url, hostPort.substring(colonPos+1)); 359 throw new DirectoryException(INVALID_ATTRIBUTE_SYNTAX, message); 360 } 361 catch (IllegalArgumentException e) 362 { 363 LocalizableMessage message = ERR_LDAPURL_INVALID_PORT.get(url, port); 364 throw new DirectoryException(INVALID_ATTRIBUTE_SYNTAX, message); 365 } 366 } 367 } 368 369 370 // Move past the slash. If we're at or past the end of the 371 // string, then we're done. 372 pos++; 373 if (pos > length) 374 { 375 return new LDAPURL(scheme, host, port, DEFAULT_BASE_DN, null, 376 DEFAULT_SEARCH_SCOPE, DEFAULT_SEARCH_FILTER, 377 null); 378 } 379 startPos = pos; 380 381 382 // The next delimiter should be a question mark. If there isn't 383 // one, then the rest of the value must be the base DN. 384 String baseDNString = null; 385 pos = url.indexOf('?', startPos); 386 if (pos < 0) 387 { 388 baseDNString = urlDecode(url.substring(startPos)); 389 startPos = length; 390 } 391 else 392 { 393 baseDNString = urlDecode(url.substring(startPos, pos)); 394 startPos = pos+1; 395 } 396 397 DN baseDN; 398 if (fullyDecode) 399 { 400 baseDN = DN.valueOf(baseDNString); 401 } 402 else 403 { 404 baseDN = null; 405 } 406 407 408 if (startPos >= length) 409 { 410 if (fullyDecode) 411 { 412 return new LDAPURL(scheme, host, port, baseDN, null, 413 DEFAULT_SEARCH_SCOPE, 414 DEFAULT_SEARCH_FILTER, null); 415 } 416 else 417 { 418 return new LDAPURL(scheme, host, port, baseDNString, null, 419 DEFAULT_SEARCH_SCOPE, null, null); 420 } 421 } 422 423 424 // Find the next question mark (or the end of the string if there 425 // aren't any more) and get the attribute list from it. 426 String attrsString; 427 pos = url.indexOf('?', startPos); 428 if (pos < 0) 429 { 430 attrsString = url.substring(startPos); 431 startPos = length; 432 } 433 else 434 { 435 attrsString = url.substring(startPos, pos); 436 startPos = pos+1; 437 } 438 439 LinkedHashSet<String> attributes = new LinkedHashSet<>(); 440 StringTokenizer tokenizer = new StringTokenizer(attrsString, ","); 441 while (tokenizer.hasMoreTokens()) 442 { 443 attributes.add(urlDecode(tokenizer.nextToken())); 444 } 445 446 if (startPos >= length) 447 { 448 if (fullyDecode) 449 { 450 return new LDAPURL(scheme, host, port, baseDN, attributes, 451 DEFAULT_SEARCH_SCOPE, 452 DEFAULT_SEARCH_FILTER, null); 453 } 454 else 455 { 456 return new LDAPURL(scheme, host, port, baseDNString, 457 attributes, DEFAULT_SEARCH_SCOPE, null, 458 null); 459 } 460 } 461 462 463 // Find the next question mark (or the end of the string if there 464 // aren't any more) and get the scope from it. 465 String scopeString; 466 pos = url.indexOf('?', startPos); 467 if (pos < 0) 468 { 469 scopeString = toLowerCase(urlDecode(url.substring(startPos))); 470 startPos = length; 471 } 472 else 473 { 474 scopeString = 475 toLowerCase(urlDecode(url.substring(startPos, pos))); 476 startPos = pos+1; 477 } 478 479 SearchScope scope; 480 if (scopeString.equals("")) 481 { 482 scope = DEFAULT_SEARCH_SCOPE; 483 } 484 else if (scopeString.equals("base")) 485 { 486 scope = SearchScope.BASE_OBJECT; 487 } 488 else if (scopeString.equals("one")) 489 { 490 scope = SearchScope.SINGLE_LEVEL; 491 } 492 else if (scopeString.equals("sub")) 493 { 494 scope = SearchScope.WHOLE_SUBTREE; 495 } 496 else if (scopeString.equals("subord") || 497 scopeString.equals("subordinate")) 498 { 499 scope = SearchScope.SUBORDINATES; 500 } 501 else 502 { 503 LocalizableMessage message = ERR_LDAPURL_INVALID_SCOPE_STRING.get(url, scopeString); 504 throw new DirectoryException( 505 ResultCode.INVALID_ATTRIBUTE_SYNTAX, message); 506 } 507 508 if (startPos >= length) 509 { 510 if (fullyDecode) 511 { 512 return new LDAPURL(scheme, host, port, baseDN, attributes, 513 scope, DEFAULT_SEARCH_FILTER, null); 514 } 515 else 516 { 517 return new LDAPURL(scheme, host, port, baseDNString, 518 attributes, scope, null, null); 519 } 520 } 521 522 523 // Find the next question mark (or the end of the string if there 524 // aren't any more) and get the filter from it. 525 String filterString; 526 pos = url.indexOf('?', startPos); 527 if (pos < 0) 528 { 529 filterString = urlDecode(url.substring(startPos)); 530 startPos = length; 531 } 532 else 533 { 534 filterString = urlDecode(url.substring(startPos, pos)); 535 startPos = pos+1; 536 } 537 538 SearchFilter filter; 539 if (fullyDecode) 540 { 541 if (filterString.equals("")) 542 { 543 filter = DEFAULT_SEARCH_FILTER; 544 } 545 else 546 { 547 filter = SearchFilter.createFilterFromString(filterString); 548 } 549 550 if (startPos >= length) 551 { 552 if (fullyDecode) 553 { 554 return new LDAPURL(scheme, host, port, baseDN, attributes, 555 scope, filter, null); 556 } 557 else 558 { 559 return new LDAPURL(scheme, host, port, baseDNString, 560 attributes, scope, filterString, null); 561 } 562 } 563 } 564 else 565 { 566 filter = null; 567 } 568 569 570 // The rest of the string must be the set of extensions. 571 String extensionsString = url.substring(startPos); 572 LinkedList<String> extensions = new LinkedList<>(); 573 tokenizer = new StringTokenizer(extensionsString, ","); 574 while (tokenizer.hasMoreTokens()) 575 { 576 extensions.add(urlDecode(tokenizer.nextToken())); 577 } 578 579 580 if (fullyDecode) 581 { 582 return new LDAPURL(scheme, host, port, baseDN, attributes, 583 scope, filter, extensions); 584 } 585 else 586 { 587 return new LDAPURL(scheme, host, port, baseDNString, attributes, 588 scope, filterString, extensions); 589 } 590 } 591 592 593 594 /** 595 * Converts the provided string to a form that has decoded "special" 596 * characters that have been encoded for use in an LDAP URL. 597 * 598 * @param s The string to be decoded. 599 * 600 * @return The decoded string. 601 * 602 * @throws DirectoryException If a problem occurs while attempting 603 * to decode the contents of the 604 * provided string. 605 */ 606 static String urlDecode(String s) throws DirectoryException 607 { 608 if (s == null) 609 { 610 return ""; 611 } 612 613 byte[] stringBytes = getBytes(s); 614 int length = stringBytes.length; 615 byte[] decodedBytes = new byte[length]; 616 int pos = 0; 617 618 for (int i=0; i < length; i++) 619 { 620 if (stringBytes[i] == '%') 621 { 622 // There must be at least two bytes left. If not, then that's 623 // a problem. 624 if (i+2 > length) 625 { 626 LocalizableMessage message = ERR_LDAPURL_PERCENT_TOO_CLOSE_TO_END.get(s, i); 627 throw new DirectoryException( 628 ResultCode.INVALID_ATTRIBUTE_SYNTAX, message); 629 } 630 631 byte b; 632 switch (stringBytes[++i]) 633 { 634 case '0': 635 b = (byte) 0x00; 636 break; 637 case '1': 638 b = (byte) 0x10; 639 break; 640 case '2': 641 b = (byte) 0x20; 642 break; 643 case '3': 644 b = (byte) 0x30; 645 break; 646 case '4': 647 b = (byte) 0x40; 648 break; 649 case '5': 650 b = (byte) 0x50; 651 break; 652 case '6': 653 b = (byte) 0x60; 654 break; 655 case '7': 656 b = (byte) 0x70; 657 break; 658 case '8': 659 b = (byte) 0x80; 660 break; 661 case '9': 662 b = (byte) 0x90; 663 break; 664 case 'a': 665 case 'A': 666 b = (byte) 0xA0; 667 break; 668 case 'b': 669 case 'B': 670 b = (byte) 0xB0; 671 break; 672 case 'c': 673 case 'C': 674 b = (byte) 0xC0; 675 break; 676 case 'd': 677 case 'D': 678 b = (byte) 0xD0; 679 break; 680 case 'e': 681 case 'E': 682 b = (byte) 0xE0; 683 break; 684 case 'f': 685 case 'F': 686 b = (byte) 0xF0; 687 break; 688 default: 689 LocalizableMessage message = ERR_LDAPURL_INVALID_HEX_BYTE.get(s, i); 690 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message); 691 } 692 693 switch (stringBytes[++i]) 694 { 695 case '0': 696 break; 697 case '1': 698 b |= 0x01; 699 break; 700 case '2': 701 b |= 0x02; 702 break; 703 case '3': 704 b |= 0x03; 705 break; 706 case '4': 707 b |= 0x04; 708 break; 709 case '5': 710 b |= 0x05; 711 break; 712 case '6': 713 b |= 0x06; 714 break; 715 case '7': 716 b |= 0x07; 717 break; 718 case '8': 719 b |= 0x08; 720 break; 721 case '9': 722 b |= 0x09; 723 break; 724 case 'a': 725 case 'A': 726 b |= 0x0A; 727 break; 728 case 'b': 729 case 'B': 730 b |= 0x0B; 731 break; 732 case 'c': 733 case 'C': 734 b |= 0x0C; 735 break; 736 case 'd': 737 case 'D': 738 b |= 0x0D; 739 break; 740 case 'e': 741 case 'E': 742 b |= 0x0E; 743 break; 744 case 'f': 745 case 'F': 746 b |= 0x0F; 747 break; 748 default: 749 LocalizableMessage message = ERR_LDAPURL_INVALID_HEX_BYTE.get(s, i); 750 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message); 751 } 752 753 decodedBytes[pos++] = b; 754 } 755 else 756 { 757 decodedBytes[pos++] = stringBytes[i]; 758 } 759 } 760 761 try 762 { 763 return new String(decodedBytes, 0, pos, "UTF-8"); 764 } 765 catch (Exception e) 766 { 767 logger.traceException(e); 768 769 // This should never happen. 770 LocalizableMessage message = ERR_LDAPURL_CANNOT_CREATE_UTF8_STRING.get( 771 getExceptionMessage(e)); 772 throw new DirectoryException( 773 ResultCode.INVALID_ATTRIBUTE_SYNTAX, message); 774 } 775 } 776 777 778 779 /** 780 * Encodes the provided string portion for inclusion in an LDAP URL 781 * and appends it to the provided buffer. 782 * 783 * @param s The string portion to be encoded. 784 * @param isExtension Indicates whether the provided component is 785 * an extension and therefore needs to have 786 * commas encoded. 787 * @param buffer The buffer to which the information should 788 * be appended. 789 */ 790 private static void urlEncode(String s, boolean isExtension, 791 StringBuilder buffer) 792 { 793 if (s == null) 794 { 795 return; 796 } 797 798 int length = s.length(); 799 800 for (int i=0; i < length; i++) 801 { 802 char c = s.charAt(i); 803 if (isAlpha(c) || isDigit(c)) 804 { 805 buffer.append(c); 806 continue; 807 } 808 809 if (c == ',') 810 { 811 if (isExtension) 812 { 813 hexEncode(c, buffer); 814 } 815 else 816 { 817 buffer.append(c); 818 } 819 820 continue; 821 } 822 823 switch (c) 824 { 825 case '-': 826 case '.': 827 case '_': 828 case '~': 829 case ':': 830 case '/': 831 case '#': 832 case '[': 833 case ']': 834 case '@': 835 case '!': 836 case '$': 837 case '&': 838 case '\'': 839 case '(': 840 case ')': 841 case '*': 842 case '+': 843 case ';': 844 case '=': 845 buffer.append(c); 846 break; 847 default: 848 hexEncode(c, buffer); 849 break; 850 } 851 } 852 } 853 854 855 856 /** 857 * Appends a percent-encoded representation of the provided 858 * character to the given buffer. 859 * 860 * @param c The character to add to the buffer. 861 * @param buffer The buffer to which the percent-encoded 862 * representation should be written. 863 */ 864 private static void hexEncode(char c, StringBuilder buffer) 865 { 866 if ((c & (byte) 0xFF) == c) 867 { 868 // It's a single byte. 869 buffer.append('%'); 870 buffer.append(byteToHex((byte) c)); 871 } 872 else 873 { 874 // It requires two bytes, and each should be prefixed by a 875 // percent sign. 876 buffer.append('%'); 877 byte b1 = (byte) ((c >>> 8) & 0xFF); 878 buffer.append(byteToHex(b1)); 879 880 buffer.append('%'); 881 byte b2 = (byte) (c & 0xFF); 882 buffer.append(byteToHex(b2)); 883 } 884 } 885 886 887 888 /** 889 * Retrieves the scheme for this LDAP URL. 890 * 891 * @return The scheme for this LDAP URL. 892 */ 893 public String getScheme() 894 { 895 return scheme; 896 } 897 898 899 900 /** 901 * Specifies the scheme for this LDAP URL. 902 * 903 * @param scheme The scheme for this LDAP URL. 904 */ 905 public void setScheme(String scheme) 906 { 907 if (scheme == null) 908 { 909 this.scheme = DEFAULT_SCHEME; 910 } 911 else 912 { 913 this.scheme = scheme; 914 } 915 } 916 917 918 919 /** 920 * Retrieves the host for this LDAP URL. 921 * 922 * @return The host for this LDAP URL, or <CODE>null</CODE> if none 923 * was provided. 924 */ 925 public String getHost() 926 { 927 return host; 928 } 929 930 931 932 /** 933 * Specifies the host for this LDAP URL. 934 * 935 * @param host The host for this LDAP URL. 936 */ 937 public void setHost(String host) 938 { 939 this.host = host; 940 } 941 942 943 944 /** 945 * Retrieves the port for this LDAP URL. 946 * 947 * @return The port for this LDAP URL. 948 */ 949 public int getPort() 950 { 951 return port; 952 } 953 954 955 956 /** 957 * Specifies the port for this LDAP URL. 958 * 959 * @param port The port for this LDAP URL. 960 */ 961 public void setPort(int port) 962 { 963 this.port = toPort(port); 964 } 965 966 private int toPort(int port) 967 { 968 if (0 < port && port <= 65535) 969 { 970 return port; 971 } 972 return DEFAULT_PORT; 973 } 974 975 /** 976 * Retrieve the raw, unprocessed base DN for this LDAP URL. 977 * 978 * @return The raw, unprocessed base DN for this LDAP URL, or 979 * <CODE>null</CODE> if none was given (in which case a 980 * default of the null DN "" should be assumed). 981 */ 982 public String getRawBaseDN() 983 { 984 return rawBaseDN; 985 } 986 987 988 989 /** 990 * Specifies the raw, unprocessed base DN for this LDAP URL. 991 * 992 * @param rawBaseDN The raw, unprocessed base DN for this LDAP 993 * URL. 994 */ 995 public void setRawBaseDN(String rawBaseDN) 996 { 997 this.rawBaseDN = rawBaseDN; 998 this.baseDN = null; 999 } 1000 1001 1002 1003 /** 1004 * Retrieves the processed DN for this LDAP URL. 1005 * 1006 * @return The processed DN for this LDAP URL. 1007 * 1008 * @throws DirectoryException If the raw base DN cannot be decoded 1009 * as a valid DN. 1010 */ 1011 public DN getBaseDN() 1012 throws DirectoryException 1013 { 1014 if (baseDN == null) 1015 { 1016 if (rawBaseDN == null || rawBaseDN.length() == 0) 1017 { 1018 return DEFAULT_BASE_DN; 1019 } 1020 1021 baseDN = DN.valueOf(rawBaseDN); 1022 } 1023 1024 return baseDN; 1025 } 1026 1027 1028 1029 /** 1030 * Specifies the base DN for this LDAP URL. 1031 * 1032 * @param baseDN The base DN for this LDAP URL. 1033 */ 1034 public void setBaseDN(DN baseDN) 1035 { 1036 if (baseDN == null) 1037 { 1038 this.baseDN = null; 1039 this.rawBaseDN = null; 1040 } 1041 else 1042 { 1043 this.baseDN = baseDN; 1044 this.rawBaseDN = baseDN.toString(); 1045 } 1046 } 1047 1048 1049 1050 /** 1051 * Retrieves the set of attributes for this LDAP URL. The contents 1052 * of the returned set may be altered by the caller. 1053 * 1054 * @return The set of attributes for this LDAP URL. 1055 */ 1056 public LinkedHashSet<String> getAttributes() 1057 { 1058 return attributes; 1059 } 1060 1061 1062 1063 /** 1064 * Retrieves the search scope for this LDAP URL. 1065 * 1066 * @return The search scope for this LDAP URL, or <CODE>null</CODE> 1067 * if none was given (in which case the base-level scope 1068 * should be assumed). 1069 */ 1070 public SearchScope getScope() 1071 { 1072 return scope; 1073 } 1074 1075 1076 1077 /** 1078 * Specifies the search scope for this LDAP URL. 1079 * 1080 * @param scope The search scope for this LDAP URL. 1081 */ 1082 public void setScope(SearchScope scope) 1083 { 1084 if (scope == null) 1085 { 1086 this.scope = DEFAULT_SEARCH_SCOPE; 1087 } 1088 else 1089 { 1090 this.scope = scope; 1091 } 1092 } 1093 1094 1095 1096 /** 1097 * Retrieves the raw, unprocessed search filter for this LDAP URL. 1098 * 1099 * @return The raw, unprocessed search filter for this LDAP URL, or 1100 * <CODE>null</CODE> if none was given (in which case a 1101 * default filter of "(objectClass=*)" should be assumed). 1102 */ 1103 public String getRawFilter() 1104 { 1105 return rawFilter; 1106 } 1107 1108 1109 1110 /** 1111 * Specifies the raw, unprocessed search filter for this LDAP URL. 1112 * 1113 * @param rawFilter The raw, unprocessed search filter for this 1114 * LDAP URL. 1115 */ 1116 public void setRawFilter(String rawFilter) 1117 { 1118 this.rawFilter = rawFilter; 1119 this.filter = null; 1120 } 1121 1122 1123 1124 /** 1125 * Retrieves the processed search filter for this LDAP URL. 1126 * 1127 * @return The processed search filter for this LDAP URL. 1128 * 1129 * @throws DirectoryException If a problem occurs while attempting 1130 * to decode the raw filter. 1131 */ 1132 public SearchFilter getFilter() 1133 throws DirectoryException 1134 { 1135 if (filter == null) 1136 { 1137 if (rawFilter == null) 1138 { 1139 filter = DEFAULT_SEARCH_FILTER; 1140 } 1141 else 1142 { 1143 filter = SearchFilter.createFilterFromString(rawFilter); 1144 } 1145 } 1146 1147 return filter; 1148 } 1149 1150 1151 1152 /** 1153 * Specifies the search filter for this LDAP URL. 1154 * 1155 * @param filter The search filter for this LDAP URL. 1156 */ 1157 public void setFilter(SearchFilter filter) 1158 { 1159 if (filter == null) 1160 { 1161 this.rawFilter = null; 1162 this.filter = null; 1163 } 1164 else 1165 { 1166 this.rawFilter = filter.toString(); 1167 this.filter = filter; 1168 } 1169 } 1170 1171 1172 1173 /** 1174 * Retrieves the set of extensions for this LDAP URL. The contents 1175 * of the returned list may be altered by the caller. 1176 * 1177 * @return The set of extensions for this LDAP URL. 1178 */ 1179 public List<String> getExtensions() 1180 { 1181 return extensions; 1182 } 1183 1184 1185 1186 /** 1187 * Indicates whether the provided entry matches the criteria defined 1188 * in this LDAP URL. 1189 * 1190 * @param entry The entry for which to make the determination. 1191 * 1192 * @return {@code true} if the provided entry does match the 1193 * criteria specified in this LDAP URL, or {@code false} if 1194 * it does not. 1195 * 1196 * @throws DirectoryException If a problem occurs while attempting 1197 * to make the determination. 1198 */ 1199 public boolean matchesEntry(Entry entry) 1200 throws DirectoryException 1201 { 1202 SearchScope scope = getScope(); 1203 if (scope == null) 1204 { 1205 scope = SearchScope.BASE_OBJECT; 1206 } 1207 1208 return entry.matchesBaseAndScope(getBaseDN(), scope) 1209 && getFilter().matchesEntry(entry); 1210 } 1211 1212 1213 1214 /** 1215 * Indicates whether the provided object is equal to this LDAP URL. 1216 * 1217 * @param o The object for which to make the determination. 1218 * 1219 * @return <CODE>true</CODE> if the object is equal to this LDAP 1220 * URL, or <CODE>false</CODE> if not. 1221 */ 1222 @Override 1223 public boolean equals(Object o) 1224 { 1225 if (o == this) 1226 { 1227 return true; 1228 } 1229 if (!(o instanceof LDAPURL)) 1230 { 1231 return false; 1232 } 1233 1234 LDAPURL url = (LDAPURL) o; 1235 return scheme.equals(url.getScheme()) 1236 && hostEquals(url) 1237 && port == url.getPort() 1238 && baseDnsEqual(url) 1239 && scope.equals(url.getScope()) 1240 && filtersEqual(url) 1241 && attributesEqual(url.getAttributes()) 1242 && extensionsEqual(url.getExtensions()); 1243 } 1244 1245 private boolean hostEquals(LDAPURL url) 1246 { 1247 if (host != null) 1248 { 1249 return host.equalsIgnoreCase(url.getHost()); 1250 } 1251 return url.getHost() == null; 1252 } 1253 1254 private boolean baseDnsEqual(LDAPURL url) 1255 { 1256 try 1257 { 1258 return getBaseDN().equals(url.getBaseDN()); 1259 } 1260 catch (Exception e) 1261 { 1262 logger.traceException(e); 1263 return Objects.equals(rawBaseDN, url.getRawBaseDN()); 1264 } 1265 } 1266 1267 private boolean filtersEqual(LDAPURL url) 1268 { 1269 try 1270 { 1271 return getFilter().equals(url.getFilter()); 1272 } 1273 catch (Exception e) 1274 { 1275 logger.traceException(e); 1276 return Objects.equals(rawFilter, url.getRawFilter()); 1277 } 1278 } 1279 1280 private boolean attributesEqual(Set<String> urlAttrs) 1281 { 1282 if (attributes.size() != urlAttrs.size()) 1283 { 1284 return false; 1285 } 1286 1287 for (String attr : attributes) 1288 { 1289 if (!urlAttrs.contains(attr) && !containsIgnoreCase(urlAttrs, attr)) 1290 { 1291 return false; 1292 } 1293 } 1294 return true; 1295 } 1296 1297 private boolean containsIgnoreCase(Set<String> urlAttrs, String attr) 1298 { 1299 for (String attr2 : urlAttrs) 1300 { 1301 if (attr.equalsIgnoreCase(attr2)) 1302 { 1303 return true; 1304 } 1305 } 1306 return false; 1307 } 1308 1309 private boolean extensionsEqual(List<String> extensions) 1310 { 1311 if (this.extensions.size() != extensions.size()) 1312 { 1313 return false; 1314 } 1315 1316 for (String ext : this.extensions) 1317 { 1318 if (!extensions.contains(ext)) 1319 { 1320 return false; 1321 } 1322 } 1323 return true; 1324 } 1325 1326 1327 1328 /** 1329 * Retrieves the hash code for this LDAP URL. 1330 * 1331 * @return The hash code for this LDAP URL. 1332 */ 1333 @Override 1334 public int hashCode() 1335 { 1336 int hashCode = 0; 1337 1338 hashCode += scheme.hashCode(); 1339 1340 if (host != null) 1341 { 1342 hashCode += toLowerCase(host).hashCode(); 1343 } 1344 1345 hashCode += port; 1346 1347 try 1348 { 1349 hashCode += getBaseDN().hashCode(); 1350 } 1351 catch (Exception e) 1352 { 1353 logger.traceException(e); 1354 1355 if (rawBaseDN != null) 1356 { 1357 hashCode += rawBaseDN.hashCode(); 1358 } 1359 } 1360 1361 hashCode += getScope().intValue(); 1362 1363 for (String attr : attributes) 1364 { 1365 hashCode += toLowerCase(attr).hashCode(); 1366 } 1367 1368 try 1369 { 1370 hashCode += getFilter().hashCode(); 1371 } 1372 catch (Exception e) 1373 { 1374 logger.traceException(e); 1375 1376 if (rawFilter != null) 1377 { 1378 hashCode += rawFilter.hashCode(); 1379 } 1380 } 1381 1382 for (String ext : extensions) 1383 { 1384 hashCode += ext.hashCode(); 1385 } 1386 1387 return hashCode; 1388 } 1389 1390 1391 1392 /** 1393 * Retrieves a string representation of this LDAP URL. 1394 * 1395 * @return A string representation of this LDAP URL. 1396 */ 1397 @Override 1398 public String toString() 1399 { 1400 StringBuilder buffer = new StringBuilder(); 1401 toString(buffer, false); 1402 return buffer.toString(); 1403 } 1404 1405 1406 1407 /** 1408 * Appends a string representation of this LDAP URL to the provided 1409 * buffer. 1410 * 1411 * @param buffer The buffer to which the information is to be 1412 * appended. 1413 * @param baseOnly Indicates whether the resulting URL string 1414 * should only include the portion up to the base 1415 * DN, omitting the attributes, scope, filter, and 1416 * extensions. 1417 */ 1418 public void toString(StringBuilder buffer, boolean baseOnly) 1419 { 1420 urlEncode(scheme, false, buffer); 1421 buffer.append("://"); 1422 1423 if (host != null) 1424 { 1425 urlEncode(host, false, buffer); 1426 buffer.append(":"); 1427 buffer.append(port); 1428 } 1429 1430 buffer.append("/"); 1431 urlEncode(rawBaseDN, false, buffer); 1432 1433 if (baseOnly) 1434 { 1435 // If there are extensions, then we need to include them. 1436 // Technically, we only have to include critical extensions, but 1437 // we'll use all of them. 1438 if (! extensions.isEmpty()) 1439 { 1440 buffer.append("????"); 1441 Iterator<String> iterator = extensions.iterator(); 1442 urlEncode(iterator.next(), true, buffer); 1443 1444 while (iterator.hasNext()) 1445 { 1446 buffer.append(","); 1447 urlEncode(iterator.next(), true, buffer); 1448 } 1449 } 1450 1451 return; 1452 } 1453 1454 buffer.append("?"); 1455 if (! attributes.isEmpty()) 1456 { 1457 Iterator<String> iterator = attributes.iterator(); 1458 urlEncode(iterator.next(), false, buffer); 1459 1460 while (iterator.hasNext()) 1461 { 1462 buffer.append(","); 1463 urlEncode(iterator.next(), false, buffer); 1464 } 1465 } 1466 1467 buffer.append("?"); 1468 switch (scope.asEnum()) 1469 { 1470 case BASE_OBJECT: 1471 buffer.append("base"); 1472 break; 1473 case SINGLE_LEVEL: 1474 buffer.append("one"); 1475 break; 1476 case WHOLE_SUBTREE: 1477 buffer.append("sub"); 1478 break; 1479 case SUBORDINATES: 1480 buffer.append("subordinate"); 1481 break; 1482 } 1483 1484 buffer.append("?"); 1485 urlEncode(rawFilter, false, buffer); 1486 1487 if (! extensions.isEmpty()) 1488 { 1489 buffer.append("?"); 1490 Iterator<String> iterator = extensions.iterator(); 1491 urlEncode(iterator.next(), true, buffer); 1492 1493 while (iterator.hasNext()) 1494 { 1495 buffer.append(","); 1496 urlEncode(iterator.next(), true, buffer); 1497 } 1498 } 1499 } 1500}