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