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