001 /* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 * 019 */ 020 package org.apache.directory.shared.ldap.util; 021 022 023 import java.io.ByteArrayOutputStream; 024 import java.io.UnsupportedEncodingException; 025 import java.net.URI; 026 import java.text.ParseException; 027 import java.util.ArrayList; 028 import java.util.HashSet; 029 import java.util.List; 030 import java.util.Set; 031 032 import org.apache.directory.shared.asn1.codec.binary.Hex; 033 import org.apache.directory.shared.i18n.I18n; 034 import org.apache.directory.shared.ldap.codec.util.HttpClientError; 035 import org.apache.directory.shared.ldap.codec.util.LdapURLEncodingException; 036 import org.apache.directory.shared.ldap.codec.util.URIException; 037 import org.apache.directory.shared.ldap.codec.util.UrlDecoderException; 038 import org.apache.directory.shared.ldap.exception.LdapInvalidDnException; 039 import org.apache.directory.shared.ldap.filter.FilterParser; 040 import org.apache.directory.shared.ldap.filter.SearchScope; 041 import org.apache.directory.shared.ldap.name.DN; 042 043 044 /** 045 * Decodes a LdapUrl, and checks that it complies with 046 * the RFC 2255. The grammar is the following : 047 * ldapurl = scheme "://" [hostport] ["/" 048 * [dn ["?" [attributes] ["?" [scope] 049 * ["?" [filter] ["?" extensions]]]]]] 050 * scheme = "ldap" 051 * attributes = attrdesc *("," attrdesc) 052 * scope = "base" / "one" / "sub" 053 * dn = DN 054 * hostport = hostport from Section 5 of RFC 1738 055 * attrdesc = AttributeDescription from Section 4.1.5 of RFC 2251 056 * filter = filter from Section 4 of RFC 2254 057 * extensions = extension *("," extension) 058 * extension = ["!"] extype ["=" exvalue] 059 * extype = token / xtoken 060 * exvalue = LDAPString 061 * token = oid from section 4.1 of RFC 2252 062 * xtoken = ("X-" / "x-") token 063 * 064 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a> 065 * @version $Rev: 923455 $, $Date: 2010-03-15 22:59:28 +0100 (Mon, 15 Mar 2010) $, 066 */ 067 public class LdapURL 068 { 069 070 // ~ Static fields/initializers 071 // ----------------------------------------------------------------- 072 073 /** The constant for "ldaps://" scheme. */ 074 public static final String LDAPS_SCHEME = "ldaps://"; 075 076 /** The constant for "ldap://" scheme. */ 077 public static final String LDAP_SCHEME = "ldap://"; 078 079 /** A null LdapURL */ 080 public static final LdapURL EMPTY_URL = new LdapURL(); 081 082 // ~ Instance fields 083 // ---------------------------------------------------------------------------- 084 085 /** The scheme */ 086 private String scheme; 087 088 /** The host */ 089 private String host; 090 091 /** The port */ 092 private int port; 093 094 /** The DN */ 095 private DN dn; 096 097 /** The attributes */ 098 private List<String> attributes; 099 100 /** The scope */ 101 private SearchScope scope; 102 103 /** The filter as a string */ 104 private String filter; 105 106 /** The extensions. */ 107 private List<Extension> extensionList; 108 109 /** Stores the LdapURL as a String */ 110 private String string; 111 112 /** Stores the LdapURL as a byte array */ 113 private byte[] bytes; 114 115 /** modal parameter that forces explicit scope rendering in toString */ 116 private boolean forceScopeRendering; 117 118 119 // ~ Constructors 120 // ------------------------------------------------------------------------------- 121 122 /** 123 * Construct an empty LdapURL 124 */ 125 public LdapURL() 126 { 127 scheme = LDAP_SCHEME; 128 host = null; 129 port = -1; 130 dn = null; 131 attributes = new ArrayList<String>(); 132 scope = SearchScope.OBJECT; 133 filter = null; 134 extensionList = new ArrayList<Extension>( 2 ); 135 } 136 137 138 /** 139 * Parse a LdapURL 140 * @param chars The chars containing the URL 141 * @throws LdapURLEncodingException If the URL is invalid 142 */ 143 public void parse( char[] chars ) throws LdapURLEncodingException 144 { 145 scheme = LDAP_SCHEME; 146 host = null; 147 port = -1; 148 dn = null; 149 attributes = new ArrayList<String>(); 150 scope = SearchScope.OBJECT; 151 filter = null; 152 extensionList = new ArrayList<Extension>( 2 ); 153 154 if ( ( chars == null ) || ( chars.length == 0 ) ) 155 { 156 host = ""; 157 return; 158 } 159 160 // ldapurl = scheme "://" [hostport] ["/" 161 // [dn ["?" [attributes] ["?" [scope] 162 // ["?" [filter] ["?" extensions]]]]]] 163 // scheme = "ldap" 164 165 int pos = 0; 166 167 // The scheme 168 if ( ( ( pos = StringTools.areEquals( chars, 0, LDAP_SCHEME ) ) == StringTools.NOT_EQUAL ) 169 && ( ( pos = StringTools.areEquals( chars, 0, LDAPS_SCHEME ) ) == StringTools.NOT_EQUAL ) ) 170 { 171 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04398 ) ); 172 } 173 else 174 { 175 scheme = new String( chars, 0, pos ); 176 } 177 178 // The hostport 179 if ( ( pos = parseHostPort( chars, pos ) ) == -1 ) 180 { 181 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04399 ) ); 182 } 183 184 if ( pos == chars.length ) 185 { 186 return; 187 } 188 189 // An optional '/' 190 if ( !StringTools.isCharASCII( chars, pos, '/' ) ) 191 { 192 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04400, pos, chars[pos] ) ); 193 } 194 195 pos++; 196 197 if ( pos == chars.length ) 198 { 199 return; 200 } 201 202 // An optional DN 203 if ( ( pos = parseDN( chars, pos ) ) == -1 ) 204 { 205 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04401 ) ); 206 } 207 208 if ( pos == chars.length ) 209 { 210 return; 211 } 212 213 // Optionals attributes 214 if ( !StringTools.isCharASCII( chars, pos, '?' ) ) 215 { 216 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04402, pos, chars[pos] ) ); 217 } 218 219 pos++; 220 221 if ( ( pos = parseAttributes( chars, pos ) ) == -1 ) 222 { 223 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04403 ) ); 224 } 225 226 if ( pos == chars.length ) 227 { 228 return; 229 } 230 231 // Optional scope 232 if ( !StringTools.isCharASCII( chars, pos, '?' ) ) 233 { 234 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04402, pos, chars[pos] ) ); 235 } 236 237 pos++; 238 239 if ( ( pos = parseScope( chars, pos ) ) == -1 ) 240 { 241 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04404 ) ); 242 } 243 244 if ( pos == chars.length ) 245 { 246 return; 247 } 248 249 // Optional filter 250 if ( !StringTools.isCharASCII( chars, pos, '?' ) ) 251 { 252 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04402, pos, chars[pos] ) ); 253 } 254 255 pos++; 256 257 if ( pos == chars.length ) 258 { 259 return; 260 } 261 262 if ( ( pos = parseFilter( chars, pos ) ) == -1 ) 263 { 264 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04405 ) ); 265 } 266 267 if ( pos == chars.length ) 268 { 269 return; 270 } 271 272 // Optional extensions 273 if ( !StringTools.isCharASCII( chars, pos, '?' ) ) 274 { 275 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04402, pos, chars[pos] ) ); 276 } 277 278 pos++; 279 280 if ( ( pos = parseExtensions( chars, pos ) ) == -1 ) 281 { 282 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04406 ) ); 283 } 284 285 if ( pos == chars.length ) 286 { 287 return; 288 } 289 else 290 { 291 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04407 ) ); 292 } 293 } 294 295 296 /** 297 * Create a new LdapURL from a String after having parsed it. 298 * 299 * @param string TheString that contains the LDAPURL 300 * @throws LdapURLEncodingException If the String does not comply with RFC 2255 301 */ 302 public LdapURL( String string ) throws LdapURLEncodingException 303 { 304 if ( string == null ) 305 { 306 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04408 ) ); 307 } 308 309 try 310 { 311 bytes = string.getBytes( "UTF-8" ); 312 this.string = string; 313 parse( string.toCharArray() ); 314 } 315 catch ( UnsupportedEncodingException uee ) 316 { 317 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04409, string ) ); 318 } 319 } 320 321 322 /** 323 * Create a new LdapURL after having parsed it. 324 * 325 * @param bytes The byte buffer that contains the LDAPURL 326 * @throws LdapURLEncodingException If the byte array does not comply with RFC 2255 327 */ 328 public LdapURL( byte[] bytes ) throws LdapURLEncodingException 329 { 330 if ( ( bytes == null ) || ( bytes.length == 0 ) ) 331 { 332 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04410 ) ); 333 } 334 335 string = StringTools.utf8ToString( bytes ); 336 337 this.bytes = new byte[bytes.length]; 338 System.arraycopy( bytes, 0, this.bytes, 0, bytes.length ); 339 340 parse( string.toCharArray() ); 341 } 342 343 344 // ~ Methods 345 // ------------------------------------------------------------------------------------ 346 347 /** 348 * Parse this rule : <br> 349 * <p> 350 * <host> ::= <hostname> ':' <hostnumber><br> 351 * <hostname> ::= *[ <domainlabel> "." ] <toplabel><br> 352 * <domainlabel> ::= <alphadigit> | <alphadigit> *[ 353 * <alphadigit> | "-" ] <alphadigit><br> 354 * <toplabel> ::= <alpha> | <alpha> *[ <alphadigit> | 355 * "-" ] <alphadigit><br> 356 * <hostnumber> ::= <digits> "." <digits> "." 357 * <digits> "." <digits> 358 * </p> 359 * 360 * @param chars The buffer to parse 361 * @param pos The current position in the byte buffer 362 * @return The new position in the byte buffer, or -1 if the rule does not 363 * apply to the byte buffer TODO check that the topLabel is valid 364 * (it must start with an alpha) 365 */ 366 private int parseHost( char[] chars, int pos ) 367 { 368 369 int start = pos; 370 boolean hadDot = false; 371 boolean hadMinus = false; 372 boolean isHostNumber = true; 373 boolean invalidIp = false; 374 int nbDots = 0; 375 int[] ipElem = new int[4]; 376 377 // The host will be followed by a '/' or a ':', or by nothing if it's 378 // the end. 379 // We will search the end of the host part, and we will check some 380 // elements. 381 if ( StringTools.isCharASCII( chars, pos, '-' ) ) 382 { 383 384 // We can't have a '-' on first position 385 return -1; 386 } 387 388 while ( ( pos < chars.length ) && ( chars[pos] != ':' ) && ( chars[pos] != '/' ) ) 389 { 390 391 if ( StringTools.isCharASCII( chars, pos, '.' ) ) 392 { 393 394 if ( ( hadMinus ) || ( hadDot ) ) 395 { 396 397 // We already had a '.' just before : this is not allowed. 398 // Or we had a '-' before a '.' : ths is not allowed either. 399 return -1; 400 } 401 402 // Let's check the string we had before the dot. 403 if ( isHostNumber ) 404 { 405 406 if ( nbDots < 4 ) 407 { 408 409 // We had only digits. It may be an IP adress? Check it 410 if ( ipElem[nbDots] > 65535 ) 411 { 412 invalidIp = true; 413 } 414 } 415 } 416 417 hadDot = true; 418 nbDots++; 419 pos++; 420 continue; 421 } 422 else 423 { 424 425 if ( hadDot && StringTools.isCharASCII( chars, pos, '-' ) ) 426 { 427 428 // We can't have a '-' just after a '.' 429 return -1; 430 } 431 432 hadDot = false; 433 } 434 435 if ( StringTools.isDigit( chars, pos ) ) 436 { 437 438 if ( isHostNumber && ( nbDots < 4 ) ) 439 { 440 ipElem[nbDots] = ( ipElem[nbDots] * 10 ) + ( chars[pos] - '0' ); 441 442 if ( ipElem[nbDots] > 65535 ) 443 { 444 invalidIp = true; 445 } 446 } 447 448 hadMinus = false; 449 } 450 else if ( StringTools.isAlphaDigitMinus( chars, pos ) ) 451 { 452 isHostNumber = false; 453 454 if ( StringTools.isCharASCII( chars, pos, '-' ) ) 455 { 456 hadMinus = true; 457 } 458 else 459 { 460 hadMinus = false; 461 } 462 } 463 else 464 { 465 return -1; 466 } 467 468 pos++; 469 } 470 471 if ( start == pos ) 472 { 473 474 // An empty host is valid 475 return pos; 476 } 477 478 // Checks the hostNumber 479 if ( isHostNumber ) 480 { 481 482 // As this is a host number, we must have 3 dots. 483 if ( nbDots != 3 ) 484 { 485 return -1; 486 } 487 488 if ( invalidIp ) 489 { 490 return -1; 491 } 492 } 493 494 // Check if we have a '.' or a '-' in last position 495 if ( hadDot || hadMinus ) 496 { 497 return -1; 498 } 499 500 host = new String( chars, start, pos - start ); 501 502 return pos; 503 } 504 505 506 /** 507 * Parse this rule : <br> 508 * <p> 509 * <port> ::= <digits><br> 510 * <digits> ::= <digit> <digits-or-null><br> 511 * <digits-or-null> ::= <digit> <digits-or-null> | e<br> 512 * <digit> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 513 * </p> 514 * The port must be between 0 and 65535. 515 * 516 * @param chars The buffer to parse 517 * @param pos The current position in the byte buffer 518 * @return The new position in the byte buffer, or -1 if the rule does not 519 * apply to the byte buffer 520 */ 521 private int parsePort( char[] chars, int pos ) 522 { 523 524 if ( !StringTools.isDigit( chars, pos ) ) 525 { 526 return -1; 527 } 528 529 port = chars[pos] - '0'; 530 531 pos++; 532 533 while ( StringTools.isDigit( chars, pos ) ) 534 { 535 port = ( port * 10 ) + ( chars[pos] - '0' ); 536 537 if ( port > 65535 ) 538 { 539 return -1; 540 } 541 542 pos++; 543 } 544 545 return pos; 546 } 547 548 549 /** 550 * Parse this rule : <br> 551 * <p> 552 * <hostport> ::= <host> ':' <port> 553 * </p> 554 * 555 * @param chars The char array to parse 556 * @param pos The current position in the byte buffer 557 * @return The new position in the byte buffer, or -1 if the rule does not 558 * apply to the byte buffer 559 */ 560 private int parseHostPort( char[] chars, int pos ) 561 { 562 int hostPos = pos; 563 564 if ( ( pos = parseHost( chars, pos ) ) == -1 ) 565 { 566 return -1; 567 } 568 569 // We may have a port. 570 if ( StringTools.isCharASCII( chars, pos, ':' ) ) 571 { 572 if ( pos == hostPos ) 573 { 574 // We should not have a port if we have no host 575 return -1; 576 } 577 578 pos++; 579 } 580 else 581 { 582 return pos; 583 } 584 585 // As we have a ':', we must have a valid port (between 0 and 65535). 586 if ( ( pos = parsePort( chars, pos ) ) == -1 ) 587 { 588 return -1; 589 } 590 591 return pos; 592 } 593 594 595 /** 596 * From commons-httpclients. Converts the byte array of HTTP content 597 * characters to a string. If the specified charset is not supported, 598 * default system encoding is used. 599 * 600 * @param data the byte array to be encoded 601 * @param offset the index of the first byte to encode 602 * @param length the number of bytes to encode 603 * @param charset the desired character encoding 604 * @return The result of the conversion. 605 * @since 3.0 606 */ 607 public static String getString( final byte[] data, int offset, int length, String charset ) 608 { 609 if ( data == null ) 610 { 611 throw new IllegalArgumentException( I18n.err( I18n.ERR_04411 ) ); 612 } 613 614 if ( charset == null || charset.length() == 0 ) 615 { 616 throw new IllegalArgumentException( I18n.err( I18n.ERR_04412 ) ); 617 } 618 619 try 620 { 621 return new String( data, offset, length, charset ); 622 } 623 catch ( UnsupportedEncodingException e ) 624 { 625 return new String( data, offset, length ); 626 } 627 } 628 629 630 /** 631 * From commons-httpclients. Converts the byte array of HTTP content 632 * characters to a string. If the specified charset is not supported, 633 * default system encoding is used. 634 * 635 * @param data the byte array to be encoded 636 * @param charset the desired character encoding 637 * @return The result of the conversion. 638 * @since 3.0 639 */ 640 public static String getString( final byte[] data, String charset ) 641 { 642 return getString( data, 0, data.length, charset ); 643 } 644 645 646 /** 647 * Converts the specified string to byte array of ASCII characters. 648 * 649 * @param data the string to be encoded 650 * @return The string as a byte array. 651 * @since 3.0 652 */ 653 public static byte[] getAsciiBytes( final String data ) 654 { 655 656 if ( data == null ) 657 { 658 throw new IllegalArgumentException( I18n.err( I18n.ERR_04411 ) ); 659 } 660 661 try 662 { 663 return data.getBytes( "US-ASCII" ); 664 } 665 catch ( UnsupportedEncodingException e ) 666 { 667 throw new HttpClientError( I18n.err( I18n.ERR_04413 ) ); 668 } 669 } 670 671 672 /** 673 * From commons-codec. Decodes an array of URL safe 7-bit characters into an 674 * array of original bytes. Escaped characters are converted back to their 675 * original representation. 676 * 677 * @param bytes array of URL safe characters 678 * @return array of original bytes 679 * @throws UrlDecoderException Thrown if URL decoding is unsuccessful 680 */ 681 private static final byte[] decodeUrl( byte[] bytes ) throws UrlDecoderException 682 { 683 if ( bytes == null ) 684 { 685 return StringTools.EMPTY_BYTES; 686 } 687 688 ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 689 690 for ( int i = 0; i < bytes.length; i++ ) 691 { 692 int b = bytes[i]; 693 694 if ( b == '%' ) 695 { 696 try 697 { 698 int u = Character.digit( ( char ) bytes[++i], 16 ); 699 int l = Character.digit( ( char ) bytes[++i], 16 ); 700 701 if ( u == -1 || l == -1 ) 702 { 703 throw new UrlDecoderException( I18n.err( I18n.ERR_04414 ) ); 704 } 705 706 buffer.write( ( char ) ( ( u << 4 ) + l ) ); 707 } 708 catch ( ArrayIndexOutOfBoundsException e ) 709 { 710 throw new UrlDecoderException( I18n.err( I18n.ERR_04414 ) ); 711 } 712 } 713 else 714 { 715 buffer.write( b ); 716 } 717 } 718 719 return buffer.toByteArray(); 720 } 721 722 723 /** 724 * From commons-httpclients. Unescape and decode a given string regarded as 725 * an escaped string with the default protocol charset. 726 * 727 * @param escaped a string 728 * @return the unescaped string 729 * @throws URIException if the string cannot be decoded (invalid) 730 * @see URI#getDefaultProtocolCharset 731 */ 732 private static String decode( String escaped ) throws URIException 733 { 734 try 735 { 736 byte[] rawdata = decodeUrl( getAsciiBytes( escaped ) ); 737 return getString( rawdata, "UTF-8" ); 738 } 739 catch ( UrlDecoderException e ) 740 { 741 throw new URIException( e.getMessage() ); 742 } 743 } 744 745 746 /** 747 * Parse a string and check that it complies with RFC 2253. Here, we will 748 * just call the DN parser to do the job. 749 * 750 * @param chars The char array to be checked 751 * @param pos the starting position 752 * @return -1 if the char array does not contains a DN 753 */ 754 private int parseDN( char[] chars, int pos ) 755 { 756 757 int end = pos; 758 759 for ( int i = pos; ( i < chars.length ) && ( chars[i] != '?' ); i++ ) 760 { 761 end++; 762 } 763 764 try 765 { 766 dn = new DN( decode( new String( chars, pos, end - pos ) ) ); 767 } 768 catch ( URIException ue ) 769 { 770 return -1; 771 } 772 catch ( LdapInvalidDnException de ) 773 { 774 return -1; 775 } 776 777 return end; 778 } 779 780 781 /** 782 * Parse the attributes part 783 * 784 * @param chars The char array to be checked 785 * @param pos the starting position 786 * @return -1 if the char array does not contains attributes 787 */ 788 private int parseAttributes( char[] chars, int pos ) 789 { 790 791 int start = pos; 792 int end = pos; 793 Set<String> hAttributes = new HashSet<String>(); 794 boolean hadComma = false; 795 796 try 797 { 798 799 for ( int i = pos; ( i < chars.length ) && ( chars[i] != '?' ); i++ ) 800 { 801 802 if ( StringTools.isCharASCII( chars, i, ',' ) ) 803 { 804 hadComma = true; 805 806 if ( ( end - start ) == 0 ) 807 { 808 809 // An attributes must not be null 810 return -1; 811 } 812 else 813 { 814 String attribute = null; 815 816 // get the attribute. It must not be blank 817 attribute = new String( chars, start, end - start ).trim(); 818 819 if ( attribute.length() == 0 ) 820 { 821 return -1; 822 } 823 824 String decodedAttr = decode( attribute ); 825 826 if ( !hAttributes.contains( decodedAttr ) ) 827 { 828 attributes.add( decodedAttr ); 829 hAttributes.add( decodedAttr ); 830 } 831 } 832 833 start = i + 1; 834 } 835 else 836 { 837 hadComma = false; 838 } 839 840 end++; 841 } 842 843 if ( hadComma ) 844 { 845 846 // We are not allowed to have a comma at the end of the 847 // attributes 848 return -1; 849 } 850 else 851 { 852 853 if ( end == start ) 854 { 855 856 // We don't have any attributes. This is valid. 857 return end; 858 } 859 860 // Store the last attribute 861 // get the attribute. It must not be blank 862 String attribute = null; 863 864 attribute = new String( chars, start, end - start ).trim(); 865 866 if ( attribute.length() == 0 ) 867 { 868 return -1; 869 } 870 871 String decodedAttr = decode( attribute ); 872 873 if ( !hAttributes.contains( decodedAttr ) ) 874 { 875 attributes.add( decodedAttr ); 876 hAttributes.add( decodedAttr ); 877 } 878 } 879 880 return end; 881 } 882 catch ( URIException ue ) 883 { 884 return -1; 885 } 886 } 887 888 889 /** 890 * Parse the filter part. We will use the FilterParserImpl class 891 * 892 * @param chars The char array to be checked 893 * @param pos the starting position 894 * @return -1 if the char array does not contains a filter 895 */ 896 private int parseFilter( char[] chars, int pos ) 897 { 898 899 int end = pos; 900 901 for ( int i = pos; ( i < chars.length ) && ( chars[i] != '?' ); i++ ) 902 { 903 end++; 904 } 905 906 if ( end == pos ) 907 { 908 // We have no filter 909 return end; 910 } 911 912 try 913 { 914 filter = decode( new String( chars, pos, end - pos ) ); 915 FilterParser.parse( filter ); 916 } 917 catch ( URIException ue ) 918 { 919 return -1; 920 } 921 catch ( ParseException pe ) 922 { 923 return -1; 924 } 925 926 return end; 927 } 928 929 930 /** 931 * Parse the scope part. 932 * 933 * @param chars The char array to be checked 934 * @param pos the starting position 935 * @return -1 if the char array does not contains a scope 936 */ 937 private int parseScope( char[] chars, int pos ) 938 { 939 940 if ( StringTools.isCharASCII( chars, pos, 'b' ) || StringTools.isCharASCII( chars, pos, 'B' ) ) 941 { 942 pos++; 943 944 if ( StringTools.isCharASCII( chars, pos, 'a' ) || StringTools.isCharASCII( chars, pos, 'A' ) ) 945 { 946 pos++; 947 948 if ( StringTools.isCharASCII( chars, pos, 's' ) || StringTools.isCharASCII( chars, pos, 'S' ) ) 949 { 950 pos++; 951 952 if ( StringTools.isCharASCII( chars, pos, 'e' ) || StringTools.isCharASCII( chars, pos, 'E' ) ) 953 { 954 pos++; 955 scope = SearchScope.OBJECT; 956 return pos; 957 } 958 } 959 } 960 } 961 else if ( StringTools.isCharASCII( chars, pos, 'o' ) || StringTools.isCharASCII( chars, pos, 'O' ) ) 962 { 963 pos++; 964 965 if ( StringTools.isCharASCII( chars, pos, 'n' ) || StringTools.isCharASCII( chars, pos, 'N' ) ) 966 { 967 pos++; 968 969 if ( StringTools.isCharASCII( chars, pos, 'e' ) || StringTools.isCharASCII( chars, pos, 'E' ) ) 970 { 971 pos++; 972 973 scope = SearchScope.ONELEVEL; 974 return pos; 975 } 976 } 977 } 978 else if ( StringTools.isCharASCII( chars, pos, 's' ) || StringTools.isCharASCII( chars, pos, 'S' ) ) 979 { 980 pos++; 981 982 if ( StringTools.isCharASCII( chars, pos, 'u' ) || StringTools.isCharASCII( chars, pos, 'U' ) ) 983 { 984 pos++; 985 986 if ( StringTools.isCharASCII( chars, pos, 'b' ) || StringTools.isCharASCII( chars, pos, 'B' ) ) 987 { 988 pos++; 989 990 scope = SearchScope.SUBTREE; 991 return pos; 992 } 993 } 994 } 995 else if ( StringTools.isCharASCII( chars, pos, '?' ) ) 996 { 997 // An empty scope. This is valid 998 return pos; 999 } 1000 else if ( pos == chars.length ) 1001 { 1002 // An empty scope at the end of the URL. This is valid 1003 return pos; 1004 } 1005 1006 // The scope is not one of "one", "sub" or "base". It's an error 1007 return -1; 1008 } 1009 1010 1011 /** 1012 * Parse extensions and critical extensions. 1013 * 1014 * The grammar is : 1015 * extensions ::= extension [ ',' extension ]* 1016 * extension ::= [ '!' ] ( token | ( 'x-' | 'X-' ) token ) ) [ '=' exvalue ] 1017 * 1018 * @param chars The char array to be checked 1019 * @param pos the starting position 1020 * @return -1 if the char array does not contains valid extensions or 1021 * critical extensions 1022 */ 1023 private int parseExtensions( char[] chars, int pos ) 1024 { 1025 int start = pos; 1026 boolean isCritical = false; 1027 boolean isNewExtension = true; 1028 boolean hasValue = false; 1029 String extension = null; 1030 String value = null; 1031 1032 if ( pos == chars.length ) 1033 { 1034 return pos; 1035 } 1036 1037 try 1038 { 1039 for ( int i = pos; ( i < chars.length ); i++ ) 1040 { 1041 if ( StringTools.isCharASCII( chars, i, ',' ) ) 1042 { 1043 if ( isNewExtension ) 1044 { 1045 // a ',' is not allowed when we have already had one 1046 // or if we just started to parse the extensions. 1047 return -1; 1048 } 1049 else 1050 { 1051 if ( extension == null ) 1052 { 1053 extension = decode( new String( chars, start, i - start ) ).trim(); 1054 } 1055 else 1056 { 1057 value = decode( new String( chars, start, i - start ) ).trim(); 1058 } 1059 1060 Extension ext = new Extension( isCritical, extension, value ); 1061 extensionList.add( ext ); 1062 1063 isNewExtension = true; 1064 hasValue = false; 1065 isCritical = false; 1066 start = i + 1; 1067 extension = null; 1068 value = null; 1069 } 1070 } 1071 else if ( StringTools.isCharASCII( chars, i, '=' ) ) 1072 { 1073 if ( hasValue ) 1074 { 1075 // We may have two '=' for the same extension 1076 continue; 1077 } 1078 1079 // An optionnal value 1080 extension = decode( new String( chars, start, i - start ) ).trim(); 1081 1082 if ( extension.length() == 0 ) 1083 { 1084 // We must have an extension 1085 return -1; 1086 } 1087 1088 hasValue = true; 1089 start = i + 1; 1090 } 1091 else if ( StringTools.isCharASCII( chars, i, '!' ) ) 1092 { 1093 if ( hasValue ) 1094 { 1095 // We may have two '!' in the value 1096 continue; 1097 } 1098 1099 if ( !isNewExtension ) 1100 { 1101 // '!' must appears first 1102 return -1; 1103 } 1104 1105 isCritical = true; 1106 start++; 1107 } 1108 else 1109 { 1110 isNewExtension = false; 1111 } 1112 } 1113 1114 if ( extension == null ) 1115 { 1116 extension = decode( new String( chars, start, chars.length - start ) ).trim(); 1117 } 1118 else 1119 { 1120 value = decode( new String( chars, start, chars.length - start ) ).trim(); 1121 } 1122 1123 Extension ext = new Extension( isCritical, extension, value ); 1124 extensionList.add( ext ); 1125 1126 return chars.length; 1127 } 1128 catch ( URIException ue ) 1129 { 1130 return -1; 1131 } 1132 } 1133 1134 1135 /** 1136 * Encode a String to avoid special characters. 1137 * 1138 * 1139 * RFC 4516, section 2.1. (Percent-Encoding) 1140 * 1141 * A generated LDAP URL MUST consist only of the restricted set of 1142 * characters included in one of the following three productions defined 1143 * in [RFC3986]: 1144 * 1145 * <reserved> 1146 * <unreserved> 1147 * <pct-encoded> 1148 * 1149 * Implementations SHOULD accept other valid UTF-8 strings [RFC3629] as 1150 * input. An octet MUST be encoded using the percent-encoding mechanism 1151 * described in section 2.1 of [RFC3986] in any of these situations: 1152 * 1153 * The octet is not in the reserved set defined in section 2.2 of 1154 * [RFC3986] or in the unreserved set defined in section 2.3 of 1155 * [RFC3986]. 1156 * 1157 * It is the single Reserved character '?' and occurs inside a <dn>, 1158 * <filter>, or other element of an LDAP URL. 1159 * 1160 * It is a comma character ',' that occurs inside an <exvalue>. 1161 * 1162 * 1163 * RFC 3986, section 2.2 (Reserved Characters) 1164 * 1165 * reserved = gen-delims / sub-delims 1166 * gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" 1167 * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 1168 * / "*" / "+" / "," / ";" / "=" 1169 * 1170 * 1171 * RFC 3986, section 2.3 (Unreserved Characters) 1172 * 1173 * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 1174 * 1175 * 1176 * @param url The String to encode 1177 * @param doubleEncode Set if we need to encode the comma 1178 * @return An encoded string 1179 */ 1180 public static String urlEncode( String url, boolean doubleEncode ) 1181 { 1182 StringBuffer sb = new StringBuffer(); 1183 1184 for ( int i = 0; i < url.length(); i++ ) 1185 { 1186 char c = url.charAt( i ); 1187 1188 switch ( c ) 1189 1190 { 1191 // reserved and unreserved characters: 1192 // just append to the buffer 1193 1194 // reserved gen-delims, excluding '?' 1195 // gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" 1196 case ':': 1197 case '/': 1198 case '#': 1199 case '[': 1200 case ']': 1201 case '@': 1202 1203 // reserved sub-delims, excluding ',' 1204 // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 1205 // / "*" / "+" / "," / ";" / "=" 1206 case '!': 1207 case '$': 1208 case '&': 1209 case '\'': 1210 case '(': 1211 case ')': 1212 case '*': 1213 case '+': 1214 case ';': 1215 case '=': 1216 1217 // unreserved 1218 // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 1219 case 'a': 1220 case 'b': 1221 case 'c': 1222 case 'd': 1223 case 'e': 1224 case 'f': 1225 case 'g': 1226 case 'h': 1227 case 'i': 1228 case 'j': 1229 case 'k': 1230 case 'l': 1231 case 'm': 1232 case 'n': 1233 case 'o': 1234 case 'p': 1235 case 'q': 1236 case 'r': 1237 case 's': 1238 case 't': 1239 case 'u': 1240 case 'v': 1241 case 'w': 1242 case 'x': 1243 case 'y': 1244 case 'z': 1245 1246 case 'A': 1247 case 'B': 1248 case 'C': 1249 case 'D': 1250 case 'E': 1251 case 'F': 1252 case 'G': 1253 case 'H': 1254 case 'I': 1255 case 'J': 1256 case 'K': 1257 case 'L': 1258 case 'M': 1259 case 'N': 1260 case 'O': 1261 case 'P': 1262 case 'Q': 1263 case 'R': 1264 case 'S': 1265 case 'T': 1266 case 'U': 1267 case 'V': 1268 case 'W': 1269 case 'X': 1270 case 'Y': 1271 case 'Z': 1272 1273 case '0': 1274 case '1': 1275 case '2': 1276 case '3': 1277 case '4': 1278 case '5': 1279 case '6': 1280 case '7': 1281 case '8': 1282 case '9': 1283 1284 case '-': 1285 case '.': 1286 case '_': 1287 case '~': 1288 1289 sb.append( c ); 1290 break; 1291 1292 case ',': 1293 1294 // special case for comma 1295 if ( doubleEncode ) 1296 { 1297 sb.append( "%2c" ); 1298 } 1299 else 1300 { 1301 sb.append( c ); 1302 } 1303 break; 1304 1305 default: 1306 1307 // percent encoding 1308 byte[] bytes = StringTools.charToBytes( c ); 1309 char[] hex = Hex.encodeHex( bytes ); 1310 for ( int j = 0; j < hex.length; j++ ) 1311 { 1312 if ( j % 2 == 0 ) 1313 { 1314 sb.append( '%' ); 1315 } 1316 sb.append( hex[j] ); 1317 1318 } 1319 1320 break; 1321 } 1322 } 1323 1324 return sb.toString(); 1325 } 1326 1327 1328 /** 1329 * Get a string representation of a LdapURL. 1330 * 1331 * @return A LdapURL string 1332 * @see LdapURL#forceScopeRendering 1333 */ 1334 public String toString() 1335 { 1336 StringBuffer sb = new StringBuffer(); 1337 1338 sb.append( scheme ); 1339 1340 sb.append( ( host == null ) ? "" : host ); 1341 1342 if ( port != -1 ) 1343 { 1344 sb.append( ':' ).append( port ); 1345 } 1346 1347 if ( dn != null ) 1348 { 1349 sb.append( '/' ).append( urlEncode( dn.getName(), false ) ); 1350 1351 if ( attributes.size() != 0 || forceScopeRendering 1352 || ( ( scope != SearchScope.OBJECT ) || ( filter != null ) || ( extensionList.size() != 0 ) ) ) 1353 { 1354 sb.append( '?' ); 1355 1356 boolean isFirst = true; 1357 1358 for ( String attribute : attributes ) 1359 { 1360 if ( isFirst ) 1361 { 1362 isFirst = false; 1363 } 1364 else 1365 { 1366 sb.append( ',' ); 1367 } 1368 1369 sb.append( urlEncode( attribute, false ) ); 1370 } 1371 } 1372 1373 if ( forceScopeRendering ) 1374 { 1375 sb.append( '?' ); 1376 1377 sb.append( scope.getLdapUrlValue() ); 1378 } 1379 1380 else 1381 { 1382 if ( ( scope != SearchScope.OBJECT ) || ( filter != null ) || ( extensionList.size() != 0 ) ) 1383 { 1384 sb.append( '?' ); 1385 1386 switch ( scope ) 1387 { 1388 case ONELEVEL: 1389 case SUBTREE: 1390 sb.append( scope.getLdapUrlValue() ); 1391 break; 1392 1393 default: 1394 break; 1395 } 1396 1397 if ( ( filter != null ) || ( ( extensionList.size() != 0 ) ) ) 1398 { 1399 sb.append( "?" ); 1400 1401 if ( filter != null ) 1402 { 1403 sb.append( urlEncode( filter, false ) ); 1404 } 1405 1406 if ( ( extensionList.size() != 0 ) ) 1407 { 1408 sb.append( '?' ); 1409 1410 boolean isFirst = true; 1411 1412 if ( extensionList.size() != 0 ) 1413 { 1414 for ( Extension extension : extensionList ) 1415 { 1416 if ( !isFirst ) 1417 { 1418 sb.append( ',' ); 1419 } 1420 else 1421 { 1422 isFirst = false; 1423 } 1424 1425 if ( extension.isCritical ) 1426 { 1427 sb.append( '!' ); 1428 } 1429 sb.append( urlEncode( extension.type, false ) ); 1430 1431 if ( extension.value != null ) 1432 { 1433 sb.append( '=' ); 1434 sb.append( urlEncode( extension.value, true ) ); 1435 } 1436 } 1437 } 1438 } 1439 } 1440 } 1441 } 1442 } 1443 else 1444 { 1445 sb.append( '/' ); 1446 } 1447 1448 return sb.toString(); 1449 } 1450 1451 1452 /** 1453 * @return Returns the attributes. 1454 */ 1455 public List<String> getAttributes() 1456 { 1457 return attributes; 1458 } 1459 1460 1461 /** 1462 * @return Returns the dn. 1463 */ 1464 public DN getDn() 1465 { 1466 return dn; 1467 } 1468 1469 1470 /** 1471 * @return Returns the extensions. 1472 */ 1473 public List<Extension> getExtensions() 1474 { 1475 return extensionList; 1476 } 1477 1478 1479 /** 1480 * Gets the extension. 1481 * 1482 * @param type the extension type, case-insensitive 1483 * 1484 * @return Returns the extension, null if this URL does not contain 1485 * such an extension. 1486 */ 1487 public Extension getExtension( String type ) 1488 { 1489 for ( Extension extension : getExtensions() ) 1490 { 1491 if ( extension.getType().equalsIgnoreCase( type ) ) 1492 { 1493 return extension; 1494 } 1495 } 1496 return null; 1497 } 1498 1499 1500 /** 1501 * Gets the extension value. 1502 * 1503 * @param type the extension type, case-insensitive 1504 * 1505 * @return Returns the extension value, null if this URL does not 1506 * contain such an extension or if the extension value is null. 1507 */ 1508 public String getExtensionValue( String type ) 1509 { 1510 for ( Extension extension : getExtensions() ) 1511 { 1512 if ( extension.getType().equalsIgnoreCase( type ) ) 1513 { 1514 return extension.getValue(); 1515 } 1516 } 1517 return null; 1518 } 1519 1520 1521 /** 1522 * @return Returns the filter. 1523 */ 1524 public String getFilter() 1525 { 1526 return filter; 1527 } 1528 1529 1530 /** 1531 * @return Returns the host. 1532 */ 1533 public String getHost() 1534 { 1535 return host; 1536 } 1537 1538 1539 /** 1540 * @return Returns the port. 1541 */ 1542 public int getPort() 1543 { 1544 return port; 1545 } 1546 1547 1548 /** 1549 * Returns the scope, one of {@link SearchScope.OBJECT}, 1550 * {@link SearchScope.ONELEVEL} or {@link SearchScope.SUBTREE}. 1551 * 1552 * @return Returns the scope. 1553 */ 1554 public SearchScope getScope() 1555 { 1556 return scope; 1557 } 1558 1559 1560 /** 1561 * @return Returns the scheme. 1562 */ 1563 public String getScheme() 1564 { 1565 return scheme; 1566 } 1567 1568 1569 /** 1570 * @return the number of bytes for this LdapURL 1571 */ 1572 public int getNbBytes() 1573 { 1574 return ( bytes != null ? bytes.length : 0 ); 1575 } 1576 1577 1578 /** 1579 * @return a reference on the interned bytes representing this LdapURL 1580 */ 1581 public byte[] getBytesReference() 1582 { 1583 return bytes; 1584 } 1585 1586 1587 /** 1588 * @return a copy of the bytes representing this LdapURL 1589 */ 1590 public byte[] getBytesCopy() 1591 { 1592 if ( bytes != null ) 1593 { 1594 byte[] copy = new byte[bytes.length]; 1595 System.arraycopy( bytes, 0, copy, 0, bytes.length ); 1596 return copy; 1597 } 1598 else 1599 { 1600 return null; 1601 } 1602 } 1603 1604 1605 /** 1606 * @return the LdapURL as a String 1607 */ 1608 public String getString() 1609 { 1610 return string; 1611 } 1612 1613 1614 /** 1615 * Compute the instance's hash code 1616 * @return the instance's hash code 1617 */ 1618 public int hashCode() 1619 { 1620 return this.toString().hashCode(); 1621 } 1622 1623 1624 public boolean equals( Object obj ) 1625 { 1626 if ( this == obj ) 1627 { 1628 return true; 1629 } 1630 if ( obj == null ) 1631 { 1632 return false; 1633 } 1634 if ( getClass() != obj.getClass() ) 1635 { 1636 return false; 1637 } 1638 1639 final LdapURL other = ( LdapURL ) obj; 1640 return this.toString().equals( other.toString() ); 1641 } 1642 1643 1644 /** 1645 * Sets the scheme. Must be "ldap://" or "ldaps://", otherwise "ldap://" is assumed as default. 1646 * 1647 * @param scheme the new scheme 1648 */ 1649 public void setScheme( String scheme ) 1650 { 1651 if ( scheme != null && LDAP_SCHEME.equals( scheme ) || LDAPS_SCHEME.equals( scheme ) ) 1652 { 1653 this.scheme = scheme; 1654 } 1655 else 1656 { 1657 this.scheme = LDAP_SCHEME; 1658 } 1659 1660 } 1661 1662 1663 /** 1664 * Sets the host. 1665 * 1666 * @param host the new host 1667 */ 1668 public void setHost( String host ) 1669 { 1670 this.host = host; 1671 } 1672 1673 1674 /** 1675 * Sets the port. Must be between 1 and 65535, otherwise -1 is assumed as default. 1676 * 1677 * @param port the new port 1678 */ 1679 public void setPort( int port ) 1680 { 1681 if ( port < 1 || port > 65535 ) 1682 { 1683 this.port = -1; 1684 } 1685 else 1686 { 1687 this.port = port; 1688 } 1689 } 1690 1691 1692 /** 1693 * Sets the dn. 1694 * 1695 * @param dn the new dn 1696 */ 1697 public void setDn( DN dn ) 1698 { 1699 this.dn = dn; 1700 } 1701 1702 1703 /** 1704 * Sets the attributes, null removes all existing attributes. 1705 * 1706 * @param attributes the new attributes 1707 */ 1708 public void setAttributes( List<String> attributes ) 1709 { 1710 if ( attributes == null ) 1711 { 1712 this.attributes.clear(); 1713 } 1714 else 1715 { 1716 this.attributes = attributes; 1717 } 1718 } 1719 1720 1721 /** 1722 * Sets the scope. Must be one of {@link SearchScope.OBJECT}, 1723 * {@link SearchScope.ONELEVEL} or {@link SearchScope.SUBTREE}, 1724 * otherwise {@link SearchScope.OBJECT} is assumed as default. 1725 * 1726 * @param scope the new scope 1727 */ 1728 public void setScope( int scope ) 1729 { 1730 try 1731 { 1732 this.scope = SearchScope.getSearchScope( scope ); 1733 } 1734 catch ( IllegalArgumentException iae ) 1735 { 1736 this.scope = SearchScope.OBJECT; 1737 } 1738 } 1739 1740 1741 /** 1742 * Sets the scope. Must be one of {@link SearchScope.OBJECT}, 1743 * {@link SearchScope.ONELEVEL} or {@link SearchScope.SUBTREE}, 1744 * otherwise {@link SearchScope.OBJECT} is assumed as default. 1745 * 1746 * @param scope the new scope 1747 */ 1748 public void setScope( SearchScope scope ) 1749 { 1750 if ( scope == null ) 1751 { 1752 this.scope = SearchScope.OBJECT; 1753 } 1754 else 1755 { 1756 this.scope = scope; 1757 } 1758 } 1759 1760 1761 /** 1762 * Sets the filter. 1763 * 1764 * @param filter the new filter 1765 */ 1766 public void setFilter( String filter ) 1767 { 1768 this.filter = filter; 1769 } 1770 1771 1772 /** 1773 * If set to true forces the toString method to render the scope 1774 * regardless of optional nature. Use this when you want explicit 1775 * search URL scope rendering. 1776 * 1777 * @param forceScopeRendering the forceScopeRendering to set 1778 */ 1779 public void setForceScopeRendering( boolean forceScopeRendering ) 1780 { 1781 this.forceScopeRendering = forceScopeRendering; 1782 } 1783 1784 1785 /** 1786 * If set to true forces the toString method to render the scope 1787 * regardless of optional nature. Use this when you want explicit 1788 * search URL scope rendering. 1789 * 1790 * @return the forceScopeRendering 1791 */ 1792 public boolean isForceScopeRendering() 1793 { 1794 return forceScopeRendering; 1795 } 1796 1797 /** 1798 * An inner bean to hold extension information. 1799 * 1800 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a> 1801 * @version $Rev: 923455 $, $Date: 2010-03-15 22:59:28 +0100 (Mon, 15 Mar 2010) $ 1802 */ 1803 public static class Extension 1804 { 1805 private boolean isCritical; 1806 private String type; 1807 private String value; 1808 1809 1810 /** 1811 * Creates a new instance of Extension. 1812 * 1813 * @param isCritical true for critical extension 1814 * @param type the extension type 1815 * @param value the extension value 1816 */ 1817 public Extension( boolean isCritical, String type, String value ) 1818 { 1819 super(); 1820 this.isCritical = isCritical; 1821 this.type = type; 1822 this.value = value; 1823 } 1824 1825 1826 /** 1827 * Checks if is critical. 1828 * 1829 * @return true, if is critical 1830 */ 1831 public boolean isCritical() 1832 { 1833 return isCritical; 1834 } 1835 1836 1837 /** 1838 * Sets the critical. 1839 * 1840 * @param isCritical the new critical 1841 */ 1842 public void setCritical( boolean isCritical ) 1843 { 1844 this.isCritical = isCritical; 1845 } 1846 1847 1848 /** 1849 * Gets the type. 1850 * 1851 * @return the type 1852 */ 1853 public String getType() 1854 { 1855 return type; 1856 } 1857 1858 1859 /** 1860 * Sets the type. 1861 * 1862 * @param type the new type 1863 */ 1864 public void setType( String type ) 1865 { 1866 this.type = type; 1867 } 1868 1869 1870 /** 1871 * Gets the value. 1872 * 1873 * @return the value 1874 */ 1875 public String getValue() 1876 { 1877 return value; 1878 } 1879 1880 1881 /** 1882 * Sets the value. 1883 * 1884 * @param value the new value 1885 */ 1886 public void setValue( String value ) 1887 { 1888 this.value = value; 1889 } 1890 } 1891 1892 }