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