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