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.api.ldap.model.url; 021 022 023import java.io.ByteArrayOutputStream; 024import java.text.ParseException; 025import java.util.ArrayList; 026import java.util.Arrays; 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.api.i18n.I18n; 034import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException; 035import org.apache.directory.api.ldap.model.exception.LdapURLEncodingException; 036import org.apache.directory.api.ldap.model.exception.LdapUriException; 037import org.apache.directory.api.ldap.model.exception.UrlDecoderException; 038import org.apache.directory.api.ldap.model.filter.FilterParser; 039import org.apache.directory.api.ldap.model.message.SearchScope; 040import org.apache.directory.api.ldap.model.name.Dn; 041import org.apache.directory.api.util.Chars; 042import org.apache.directory.api.util.StringConstants; 043import org.apache.directory.api.util.Strings; 044import org.apache.directory.api.util.Unicode; 045 046 047/** 048 * Decodes a LdapUrl, and checks that it complies with 049 * the RFC 4516. The grammar is the following : 050 * <pre> 051 * ldapurl = scheme "://" [host [ ":" port]] ["/" 052 * dn ["?" [attributes] ["?" [scope] 053 * ["?" [filter] ["?" extensions]]]]] 054 * scheme = "ldap" 055 * dn = Dn 056 * attributes = attrdesc ["," attrdesc]* 057 * attrdesc = selector ["," selector]* 058 * selector = attributeSelector (from Section 4.5.1 of RFC4511) 059 * scope = "base" / "one" / "sub" 060 * extensions = extension ["," extension]* 061 * extension = ["!"] extype ["=" exvalue] 062 * extype = oid (from Section 1.4 of RFC4512) 063 * exvalue = LDAPString (from Section 4.1.2 of RFC4511) 064 * host = host from Section 3.2.2 of RFC3986 065 * port = port from Section 3.2.3 of RFC3986 066 * filter = filter from Section 3 of RFC 4515 067 * </pre> 068 * 069 * From Section 3.2.1/2 of RFC3986 070 * <pre> 071 * host = IP-literal / IPv4address / reg-name 072 * port = *DIGIT 073 * IP-literal = "[" ( IPv6address / IPvFuture ) "]" 074 * IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" ) 075 * IPv6address = 6( h16 ":" ) ls32 076 * | "::" 5( h16 ":" ) ls32 077 * | [ h16 ] "::" 4( h16 ":" ) ls32 078 * | [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 079 * | [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 080 * | [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 081 * | [ *4( h16 ":" ) h16 ] "::" ls32 082 * | [ *5( h16 ":" ) h16 ] "::" h16 083 * | [ *6( h16 ":" ) h16 ] "::" 084 * IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet 085 * dec-octet = DIGIT | [1-9] DIGIT | "1" 2DIGIT | "2" [0-4] DIGIT | "25" [0-5] 086 * reg-name = *( unreserved / pct-encoded / sub-delims ) 087 * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 088 * pct-encoded = "%" HEXDIG HEXDIG 089 * sub-delims = "!" | "$" | "&" | "'" | "(" | ")" | "*" | "+" | "," | ";" | "=" 090 * h16 = 1*4HEXDIG 091 * ls32 = ( h16 ":" h16 ) / IPv4address 092 * DIGIT = 0..9 093 * ALPHA = A-Z / a-z 094 * HEXDIG = DIGIT / A-F / a-f 095 * </pre> 096 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a> 097 */ 098public class LdapUrl 099{ 100 /** The constant for "ldaps://" scheme. */ 101 public static final String LDAPS_SCHEME = "ldaps://"; 102 103 /** The constant for "ldap://" scheme. */ 104 public static final String LDAP_SCHEME = "ldap://"; 105 106 /** A null LdapUrl */ 107 public static final LdapUrl EMPTY_URL = new LdapUrl(); 108 109 /** The scheme */ 110 private String scheme; 111 112 /** The host */ 113 private String host; 114 115 /** The port */ 116 private int port; 117 118 /** The Dn */ 119 private Dn dn; 120 121 /** The attributes */ 122 private List<String> attributes; 123 124 /** The scope */ 125 private SearchScope scope; 126 127 /** The filter as a string */ 128 private String filter; 129 130 /** The extensions. */ 131 private List<Extension> extensionList; 132 133 /** Stores the LdapUrl as a String */ 134 private String string; 135 136 /** Stores the LdapUrl as a byte array */ 137 private byte[] bytes; 138 139 /** modal parameter that forces explicit scope rendering in toString */ 140 private boolean forceScopeRendering; 141 142 /** The type of host we use */ 143 private HostTypeEnum hostType = HostTypeEnum.REGULAR_NAME; 144 145 /** A regexp for attributes */ 146 private static final Pattern ATTRIBUTE = Pattern 147 .compile( "(?:(?:\\d|[1-9]\\d*)(?:\\.(?:\\d|[1-9]\\d*))+)|(?:[a-zA-Z][a-zA-Z0-9-]*)" ); 148 149 150 /** 151 * Construct an empty LdapUrl 152 */ 153 public LdapUrl() 154 { 155 scheme = LDAP_SCHEME; 156 host = null; 157 port = -1; 158 dn = null; 159 attributes = new ArrayList<>(); 160 scope = SearchScope.OBJECT; 161 filter = null; 162 extensionList = new ArrayList<>( 2 ); 163 } 164 165 166 /** 167 * Create a new LdapUrl from a String after having parsed it. 168 * 169 * @param string TheString that contains the LdapUrl 170 * @throws LdapURLEncodingException If the String does not comply with RFC 2255 171 */ 172 public LdapUrl( String string ) throws LdapURLEncodingException 173 { 174 if ( string == null ) 175 { 176 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04408 ) ); 177 } 178 179 bytes = Strings.getBytesUtf8( string ); 180 this.string = string; 181 parse( string.toCharArray() ); 182 } 183 184 185 /** 186 * Parse a LdapUrl. 187 * 188 * @param chars The chars containing the URL 189 * @throws org.apache.directory.api.ldap.model.exception.LdapURLEncodingException If the URL is invalid 190 */ 191 private void parse( char[] chars ) throws LdapURLEncodingException 192 { 193 scheme = LDAP_SCHEME; 194 host = null; 195 port = -1; 196 dn = null; 197 attributes = new ArrayList<>(); 198 scope = SearchScope.OBJECT; 199 filter = null; 200 extensionList = new ArrayList<>( 2 ); 201 202 if ( ( chars == null ) || ( chars.length == 0 ) ) 203 { 204 host = ""; 205 return; 206 } 207 208 // ldapurl = scheme "://" [hostport] ["/" 209 // [dn ["?" [attributes] ["?" [scope] 210 // ["?" [filter] ["?" extensions]]]]]] 211 // scheme = "ldap" 212 int pos = 0; 213 214 // The scheme 215 pos = Strings.areEquals( chars, 0, LDAP_SCHEME ); 216 217 if ( pos == StringConstants.NOT_EQUAL ) 218 { 219 pos = Strings.areEquals( chars, 0, LDAPS_SCHEME ); 220 if ( pos == StringConstants.NOT_EQUAL ) 221 { 222 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04398 ) ); 223 } 224 } 225 scheme = new String( chars, 0, pos ); 226 227 // The hostport 228 pos = parseHostPort( chars, pos ); 229 if ( pos == -1 ) 230 { 231 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04399 ) ); 232 } 233 234 if ( pos == chars.length ) 235 { 236 return; 237 } 238 239 // An optional '/' 240 if ( !Chars.isCharASCII( chars, pos, '/' ) ) 241 { 242 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04400, pos, chars[pos] ) ); 243 } 244 245 pos++; 246 247 if ( pos == chars.length ) 248 { 249 return; 250 } 251 252 // An optional Dn 253 pos = parseDN( chars, pos ); 254 if ( pos == -1 ) 255 { 256 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04401 ) ); 257 } 258 259 if ( pos == chars.length ) 260 { 261 return; 262 } 263 264 // Optionals attributes 265 if ( !Chars.isCharASCII( chars, pos, '?' ) ) 266 { 267 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04402, pos, chars[pos] ) ); 268 } 269 270 pos++; 271 272 pos = parseAttributes( chars, pos ); 273 if ( pos == -1 ) 274 { 275 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04403 ) ); 276 } 277 278 if ( pos == chars.length ) 279 { 280 return; 281 } 282 283 // Optional scope 284 if ( !Chars.isCharASCII( chars, pos, '?' ) ) 285 { 286 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04402, pos, chars[pos] ) ); 287 } 288 289 pos++; 290 291 pos = parseScope( chars, pos ); 292 if ( pos == -1 ) 293 { 294 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04404 ) ); 295 } 296 297 if ( pos == chars.length ) 298 { 299 return; 300 } 301 302 // Optional filter 303 if ( !Chars.isCharASCII( chars, pos, '?' ) ) 304 { 305 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04402, pos, chars[pos] ) ); 306 } 307 308 pos++; 309 310 if ( pos == chars.length ) 311 { 312 return; 313 } 314 315 pos = parseFilter( chars, pos ); 316 if ( pos == -1 ) 317 { 318 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04405 ) ); 319 } 320 321 if ( pos == chars.length ) 322 { 323 return; 324 } 325 326 // Optional extensions 327 if ( !Chars.isCharASCII( chars, pos, '?' ) ) 328 { 329 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04402, pos, chars[pos] ) ); 330 } 331 332 pos++; 333 334 pos = parseExtensions( chars, pos ); 335 if ( pos == -1 ) 336 { 337 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04406 ) ); 338 } 339 340 if ( pos == chars.length ) 341 { 342 return; 343 } 344 else 345 { 346 throw new LdapURLEncodingException( I18n.err( I18n.ERR_04407 ) ); 347 } 348 } 349 350 351 /** 352 * Parse this rule : <br> 353 * <pre> 354 * host = IP-literal / IPv4address / reg-name 355 * port = *DIGIT 356 * <host> ::= <hostname> ':' <hostnumber><br> 357 * <hostname> ::= *[ <domainlabel> "." ] <toplabel><br> 358 * <domainlabel> ::= <alphadigit> | <alphadigit> *[ 359 * <alphadigit> | "-" ] <alphadigit><br> 360 * <toplabel> ::= <alpha> | <alpha> *[ <alphadigit> | 361 * "-" ] <alphadigit><br> 362 * <hostnumber> ::= <digits> "." <digits> "." 363 * <digits> "." <digits> 364 * </pre> 365 * 366 * @param chars The buffer to parse 367 * @param pos The current position in the byte buffer 368 * @return The new position in the byte buffer, or -1 if the rule does not 369 * apply to the byte buffer TODO check that the topLabel is valid 370 * (it must start with an alpha) 371 */ 372 private int parseHost( char[] chars, int pos ) 373 { 374 int start = pos; 375 376 // The host will be followed by a '/' or a ':', or by nothing if it's 377 // the end. 378 // We will search the end of the host part, and we will check some 379 // elements. 380 switch ( chars[pos] ) 381 { 382 case '[': 383 // This is an IP Literal address 384 return parseIpLiteral( chars, pos + 1 ); 385 386 case '0': 387 case '1': 388 case '2': 389 case '3': 390 case '4': 391 case '5': 392 case '6': 393 case '7': 394 case '8': 395 case '9': 396 // Probably an IPV4 address, but may be a reg-name 397 // try to parse an IPV4 address first 398 int currentPos = parseIPV4( chars, pos ); 399 400 if ( currentPos != -1 ) 401 { 402 host = new String( chars, start, currentPos - start ); 403 404 return currentPos; 405 } 406 //fallback to reg-name 407 408 case 'a' : case 'b' : case 'c' : case 'd' : case 'e' : 409 case 'A' : case 'B' : case 'C' : case 'D' : case 'E' : 410 case 'f' : case 'g' : case 'h' : case 'i' : case 'j' : 411 case 'F' : case 'G' : case 'H' : case 'I' : case 'J' : 412 case 'k' : case 'l' : case 'm' : case 'n' : case 'o' : 413 case 'K' : case 'L' : case 'M' : case 'N' : case 'O' : 414 case 'p' : case 'q' : case 'r' : case 's' : case 't' : 415 case 'P' : case 'Q' : case 'R' : case 'S' : case 'T' : 416 case 'u' : case 'v' : case 'w' : case 'x' : case 'y' : 417 case 'U' : case 'V' : case 'W' : case 'X' : case 'Y' : 418 case 'z' : case 'Z' : case '-' : case '.' : case '_' : 419 case '~' : case '%' : case '!' : case '$' : case '&' : 420 case '\'' : case '(' : case ')' : case '*' : case '+' : 421 case ',' : case ';' : case '=' : 422 // A reg-name 423 return parseRegName( chars, pos ); 424 425 default: 426 break; 427 } 428 429 host = new String( chars, start, pos - start ); 430 431 return pos; 432 } 433 434 435 /** 436 * parse these rules : 437 * <pre> 438 * IP-literal = "[" ( IPv6address / IPvFuture ) "]" 439 * IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" ) 440 * IPv6address = 6( h16 ":" ) ls32 441 * | "::" 5( h16 ":" ) ls32 442 * | [ h16 ] "::" 4( h16 ":" ) ls32 443 * | [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 444 * | [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 445 * | [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 446 * | [ *4( h16 ":" ) h16 ] "::" ls32 447 * | [ *5( h16 ":" ) h16 ] "::" h16 448 * | [ *6( h16 ":" ) h16 ] "::" 449 * h16 = 1*4HEXDIG 450 * ls32 = ( h16 ":" h16 ) / IPv4address 451 */ 452 private int parseIpLiteral( char[] chars, int pos ) 453 { 454 int start = pos; 455 456 if ( Chars.isCharASCII( chars, pos, 'v' ) ) 457 { 458 // This is an IPvFuture 459 pos++; 460 hostType = HostTypeEnum.IPV_FUTURE; 461 462 pos = parseIPvFuture( chars, pos ); 463 464 if ( pos != -1 ) 465 { 466 // We don't keep the last char, which is a ']' 467 host = new String( chars, start, pos - start - 1 ); 468 } 469 470 return pos; 471 } 472 else 473 { 474 // An IPV6 host 475 hostType = HostTypeEnum.IPV6; 476 477 return parseIPV6( chars, pos ); 478 } 479 } 480 481 482 /** 483 * Validates an IPv4 address. Returns true if valid. 484 * @param inet4Address the IPv4 address to validate 485 * @return true if the argument contains a valid IPv4 address 486 */ 487 public boolean isValidInet4Address( String inet4Address ) 488 { 489 return parseIPV4( inet4Address.toCharArray(), 0 ) != -1; 490 } 491 492 493 /** 494 * This code source was taken from commons.validator 1.5.0 495 * 496 * Validates an IPv6 address. Returns true if valid. 497 * @param inet6Address the IPv6 address to validate 498 * @return true if the argument contains a valid IPv6 address 499 * 500 * @since 1.4.1 501 */ 502 public boolean isValidInet6Address( String inet6Address ) 503 { 504 boolean containsCompressedZeroes = inet6Address.contains( "::" ); 505 506 if ( containsCompressedZeroes && ( inet6Address.indexOf( "::" ) != inet6Address.lastIndexOf( "::" ) ) ) 507 { 508 return false; 509 } 510 511 if ( ( inet6Address.startsWith( ":" ) && !inet6Address.startsWith( "::" ) ) 512 || ( inet6Address.endsWith( ":" ) && !inet6Address.endsWith( "::" ) ) ) 513 { 514 return false; 515 } 516 517 String[] octets = inet6Address.split( ":" ); 518 519 if ( containsCompressedZeroes ) 520 { 521 List<String> octetList = new ArrayList<>( Arrays.asList( octets ) ); 522 523 if ( inet6Address.endsWith( "::" ) ) 524 { 525 // String.split() drops ending empty segments 526 octetList.add( "" ); 527 } 528 else if ( inet6Address.startsWith( "::" ) && !octetList.isEmpty() ) 529 { 530 octetList.remove( 0 ); 531 } 532 533 octets = octetList.toArray( new String[octetList.size()] ); 534 } 535 536 if ( octets.length > 8 ) 537 { 538 return false; 539 } 540 541 int validOctets = 0; 542 int emptyOctets = 0; 543 544 for ( int index = 0; index < octets.length; index++ ) 545 { 546 String octet = octets[index]; 547 548 if ( octet.length() == 0 ) 549 { 550 emptyOctets++; 551 552 if ( emptyOctets > 1 ) 553 { 554 return false; 555 } 556 } 557 else 558 { 559 emptyOctets = 0; 560 561 if ( octet.contains( "." ) ) 562 { // contains is Java 1.5+ 563 if ( !inet6Address.endsWith( octet ) ) 564 { 565 return false; 566 } 567 568 if ( index > octets.length - 1 || index > 6 ) 569 { 570 // IPV4 occupies last two octets 571 return false; 572 } 573 574 if ( !isValidInet4Address( octet ) ) 575 { 576 return false; 577 } 578 579 validOctets += 2; 580 581 continue; 582 } 583 584 if ( octet.length() > 4 ) 585 { 586 return false; 587 } 588 589 int octetInt = 0; 590 591 try 592 { 593 octetInt = Integer.valueOf( octet, 16 ).intValue(); 594 } 595 catch ( NumberFormatException e ) 596 { 597 return false; 598 } 599 600 if ( octetInt < 0 || octetInt > 0xffff ) 601 { 602 return false; 603 } 604 } 605 606 validOctets++; 607 } 608 609 if ( validOctets < 8 && !containsCompressedZeroes ) 610 { 611 return false; 612 } 613 614 return true; 615 } 616 617 618 /** 619 * Parse the following rules : 620 * <pre> 621 * IPv6address = 6( h16 ":" ) ls32 622 * | "::" 5( h16 ":" ) ls32 623 * | [ h16 ] "::" 4( h16 ":" ) ls32 624 * | [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 625 * | [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 626 * | [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 627 * | [ *4( h16 ":" ) h16 ] "::" ls32 628 * | [ *5( h16 ":" ) h16 ] "::" h16 629 * | [ *6( h16 ":" ) h16 ] "::" 630 * h16 = 1*4HEXDIG 631 * ls32 = ( h16 ":" h16 ) / IPv4address 632 * </pre> 633 */ 634 private int parseIPV6( char[] chars, int pos ) 635 { 636 // Search for the closing ']' 637 int start = pos; 638 639 while ( !Chars.isCharASCII( chars, pos, ']' ) ) 640 { 641 pos++; 642 } 643 644 if ( Chars.isCharASCII( chars, pos, ']' ) ) 645 { 646 String hostString = new String( chars, start, pos - start ); 647 648 if ( isValidInet6Address( hostString ) ) 649 { 650 host = hostString; 651 652 return pos + 1; 653 } 654 else 655 { 656 return -1; 657 } 658 } 659 660 return -1; 661 } 662 663 664 /** 665 * Parse these rules : 666 * <pre> 667 * IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" ) 668 * </pre> 669 * (the "v" has already been parsed) 670 */ 671 private int parseIPvFuture( char[] chars, int pos ) 672 { 673 // We should have at least one hex digit 674 boolean hexFound = false; 675 676 while ( Chars.isHex( chars, pos ) ) 677 { 678 hexFound = true; 679 pos++; 680 } 681 682 if ( !hexFound ) 683 { 684 return -1; 685 } 686 687 // a dot is expected 688 if ( !Chars.isCharASCII( chars, pos, '.' ) ) 689 { 690 return -1; 691 } 692 693 // Now, we should have at least one char in unreserved / sub-delims / ":" 694 boolean valueFound = false; 695 696 while ( !Chars.isCharASCII( chars, pos, ']' ) ) 697 { 698 switch ( chars[pos] ) 699 { 700 // Unserserved 701 // ALPHA 702 case 'a' : case 'b' : case 'c' : case 'd' : case 'e' : 703 case 'A' : case 'B' : case 'C' : case 'D' : case 'E' : 704 case 'f' : case 'g' : case 'h' : case 'i' : case 'j' : 705 case 'F' : case 'G' : case 'H' : case 'I' : case 'J' : 706 case 'k' : case 'l' : case 'm' : case 'n' : case 'o' : 707 case 'K' : case 'L' : case 'M' : case 'N' : case 'O' : 708 case 'p' : case 'q' : case 'r' : case 's' : case 't' : 709 case 'P' : case 'Q' : case 'R' : case 'S' : case 'T' : 710 case 'u' : case 'v' : case 'w' : case 'x' : case 'y' : 711 case 'U' : case 'V' : case 'W' : case 'X' : case 'Y' : 712 case 'z' : case 'Z' : 713 714 // DIGITs 715 case '0' : case '1' : case '2' : case '3' : case '4' : 716 case '5' : case '6' : case '7' : case '8' : case '9' : 717 718 // others 719 case '-' : case '.' : case '_' : case '~' : 720 721 // sub-delims 722 case '!' : case '$' : case '&' : case '\'' : 723 case '(' : case ')' : case '*' : case '+' : case ',' : 724 case ';' : case '=' : 725 726 // Special case for ':' 727 case ':': 728 pos++; 729 valueFound = true; 730 break; 731 732 default: 733 // Wrong char 734 return -1; 735 } 736 } 737 738 if ( !valueFound ) 739 { 740 return -1; 741 } 742 743 return pos; 744 } 745 746 747 /** 748 * parse these rules : 749 * <pre> 750 * reg-name = *( unreserved / pct-encoded / sub-delims ) 751 * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 752 * pct-encoded = "%" HEXDIG HEXDIG 753 * sub-delims = "!" | "$" | "&" | "'" | "(" | ")" | "*" | "+" | "," | ";" | "=" 754 * HEXDIG = DIGIT / A-F / a-f 755 * </pre> 756 */ 757 private int parseRegName( char[] chars, int pos ) 758 { 759 int start = pos; 760 761 while ( !Chars.isCharASCII( chars, pos, ':' ) && !Chars.isCharASCII( chars, pos, '/' ) && ( pos < chars.length ) ) 762 { 763 switch ( chars[pos] ) 764 { 765 // Unserserved 766 // ALPHA 767 case 'a' : case 'b' : case 'c' : case 'd' : case 'e' : 768 case 'A' : case 'B' : case 'C' : case 'D' : case 'E' : 769 case 'f' : case 'g' : case 'h' : case 'i' : case 'j' : 770 case 'F' : case 'G' : case 'H' : case 'I' : case 'J' : 771 case 'k' : case 'l' : case 'm' : case 'n' : case 'o' : 772 case 'K' : case 'L' : case 'M' : case 'N' : case 'O' : 773 case 'p' : case 'q' : case 'r' : case 's' : case 't' : 774 case 'P' : case 'Q' : case 'R' : case 'S' : case 'T' : 775 case 'u' : case 'v' : case 'w' : case 'x' : case 'y' : 776 case 'U' : case 'V' : case 'W' : case 'X' : case 'Y' : 777 case 'z' : case 'Z' : 778 779 // DIGITs 780 case '0' : case '1' : case '2' : case '3' : case '4' : 781 case '5' : case '6' : case '7' : case '8' : case '9' : 782 783 // others 784 case '-' : case '.' : case '_' : case '~' : 785 786 // sub-delims 787 case '!' : case '$' : case '&' : case '\'' : 788 case '(' : case ')' : case '*' : case '+' : case ',' : 789 case ';' : case '=' : 790 pos++; 791 break; 792 793 // pct-encoded 794 case '%': 795 if ( Chars.isHex( chars, pos + 1 ) && Chars.isHex( chars, pos + 2 ) ) 796 { 797 pos += 3; 798 } 799 else 800 { 801 return -1; 802 } 803 804 default: 805 // Wrong char 806 return -1; 807 } 808 } 809 810 host = new String( chars, start, pos - start ); 811 hostType = HostTypeEnum.REGULAR_NAME; 812 813 return pos; 814 } 815 816 817 /** 818 * Parse these rules : 819 * <pre> 820 * IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet 821 * dec-octet = DIGIT | [1-9] DIGIT | "1" 2DIGIT | "2" [0-4] DIGIT | "25" [0-5] 822 * </pre> 823 * @param chars The buffer to parse 824 * @param pos The current position in the byte buffer 825 * 826 * @return The new position or -1 if this is not an IPV4 address 827 */ 828 private int parseIPV4( char[] chars, int pos ) 829 { 830 int[] ipElem = new int[4]; 831 int ipPos = pos; 832 int start = pos; 833 834 for ( int i = 0; i < 3; i++ ) 835 { 836 ipPos = parseDecOctet( chars, ipPos, ipElem, i ); 837 838 if ( ipPos == -1 ) 839 { 840 // Not an IPV4 address 841 return -1; 842 } 843 844 if ( chars[ipPos] != '.' ) 845 { 846 // Not an IPV4 address 847 return -1; 848 } 849 else 850 { 851 ipPos++; 852 } 853 } 854 855 ipPos = parseDecOctet( chars, ipPos, ipElem, 3 ); 856 857 if ( ipPos == -1 ) 858 { 859 // Not an IPV4 address 860 return -1; 861 } 862 else 863 { 864 pos = ipPos; 865 host = new String( chars, start, pos - start ); 866 hostType = HostTypeEnum.IPV4; 867 868 return pos; 869 } 870 } 871 872 873 /** 874 * Parse this rule : 875 * <pre> 876 * dec-octet = DIGIT | [1-9] DIGIT | "1" 2DIGIT | "2" [0-4] DIGIT | "25" [0-5] 877 * </pre> 878 */ 879 private int parseDecOctet( char[] chars, int pos, int[] ipElem, int octetNb ) 880 { 881 int ipElemValue = 0; 882 boolean ipElemSeen = false; 883 boolean hasHeadingZeroes = false; 884 885 while ( Chars.isDigit( chars, pos ) ) 886 { 887 ipElemSeen = true; 888 889 if ( chars[pos] == '0' ) 890 { 891 if ( hasHeadingZeroes ) 892 { 893 // Two 0 at the beginning : not allowed 894 return -1; 895 } 896 897 if ( ipElemValue > 0 ) 898 { 899 ipElemValue = ipElemValue * 10; 900 } 901 else 902 { 903 hasHeadingZeroes = true; 904 } 905 } 906 else 907 { 908 hasHeadingZeroes = false; 909 ipElemValue = ( ipElemValue * 10 ) + ( chars[pos] - '0' ); 910 } 911 912 if ( ipElemValue > 255 ) 913 { 914 return -1; 915 } 916 917 pos++; 918 } 919 920 if ( ipElemSeen ) 921 { 922 ipElem[octetNb] = ipElemValue; 923 924 return pos; 925 } 926 else 927 { 928 return -1; 929 } 930 } 931 932 933 /** 934 * Parse this rule : <br> 935 * <pre> 936 * <port> ::= <digit>+<br> 937 * <digit> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 938 * </pre> 939 * The port must be between 0 and 65535. 940 * 941 * @param chars The buffer to parse 942 * @param pos The current position in the byte buffer 943 * @return The new position in the byte buffer, or -1 if the rule does not 944 * apply to the byte buffer 945 */ 946 private int parsePort( char[] chars, int pos ) 947 { 948 949 if ( !Chars.isDigit( chars, pos ) ) 950 { 951 return -1; 952 } 953 954 port = chars[pos] - '0'; 955 956 pos++; 957 958 while ( Chars.isDigit( chars, pos ) ) 959 { 960 port = ( port * 10 ) + ( chars[pos] - '0' ); 961 962 if ( port > 65535 ) 963 { 964 return -1; 965 } 966 967 pos++; 968 } 969 970 return pos; 971 } 972 973 974 /** 975 * Parse this rule : <br> 976 * <pre> 977 * <hostport> ::= <host> [':' <port>] 978 * </pre> 979 * 980 * @param chars The char array to parse 981 * @param pos The current position in the byte buffer 982 * @return The new position in the byte buffer, or -1 if the rule does not 983 * apply to the byte buffer 984 */ 985 private int parseHostPort( char[] chars, int pos ) 986 { 987 int hostPos = pos; 988 989 pos = parseHost( chars, pos ); 990 if ( pos == -1 ) 991 { 992 return -1; 993 } 994 995 // We may have a port. 996 if ( Chars.isCharASCII( chars, pos, ':' ) ) 997 { 998 if ( pos == hostPos ) 999 { 1000 // We should not have a port if we have no host 1001 return -1; 1002 } 1003 1004 pos++; 1005 } 1006 else 1007 { 1008 return pos; 1009 } 1010 1011 // As we have a ':', we must have a valid port (between 0 and 65535). 1012 pos = parsePort( chars, pos ); 1013 if ( pos == -1 ) 1014 { 1015 return -1; 1016 } 1017 1018 return pos; 1019 } 1020 1021 1022 /** 1023 * Converts the specified string to byte array of ASCII characters. 1024 * 1025 * @param data the string to be encoded 1026 * @return The string as a byte array. 1027 * @throws org.apache.directory.api.ldap.model.exception.UrlDecoderException if encoding is not supported 1028 */ 1029 private static byte[] getAsciiBytes( final String data ) throws UrlDecoderException 1030 { 1031 if ( data == null ) 1032 { 1033 throw new IllegalArgumentException( I18n.err( I18n.ERR_04411 ) ); 1034 } 1035 1036 return Strings.getBytesUtf8( data ); 1037 } 1038 1039 1040 /** 1041 * From commons-codec. Decodes an array of URL safe 7-bit characters into an 1042 * array of original bytes. Escaped characters are converted back to their 1043 * original representation. 1044 * 1045 * @param bytes array of URL safe characters 1046 * @return array of original bytes 1047 * @throws UrlDecoderException Thrown if URL decoding is unsuccessful 1048 */ 1049 private static byte[] decodeUrl( byte[] bytes ) throws UrlDecoderException 1050 { 1051 if ( bytes == null ) 1052 { 1053 return Strings.EMPTY_BYTES; 1054 } 1055 1056 ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 1057 1058 for ( int i = 0; i < bytes.length; i++ ) 1059 { 1060 int b = bytes[i]; 1061 1062 if ( b == '%' ) 1063 { 1064 try 1065 { 1066 int u = Character.digit( ( char ) bytes[++i], 16 ); 1067 int l = Character.digit( ( char ) bytes[++i], 16 ); 1068 1069 if ( ( u == -1 ) || ( l == -1 ) ) 1070 { 1071 throw new UrlDecoderException( I18n.err( I18n.ERR_04414 ) ); 1072 } 1073 1074 buffer.write( ( char ) ( ( u << 4 ) + l ) ); 1075 } 1076 catch ( ArrayIndexOutOfBoundsException aioobe ) 1077 { 1078 throw new UrlDecoderException( I18n.err( I18n.ERR_04414 ), aioobe ); 1079 } 1080 } 1081 else 1082 { 1083 buffer.write( b ); 1084 } 1085 } 1086 1087 return buffer.toByteArray(); 1088 } 1089 1090 1091 /** 1092 * From commons-httpclients. Unescape and decode a given string regarded as 1093 * an escaped string with the default protocol charset. 1094 * 1095 * @param escaped a string 1096 * @return the unescaped string 1097 * @throws LdapUriException if the string cannot be decoded (invalid) 1098 */ 1099 private static String decode( String escaped ) throws LdapUriException 1100 { 1101 try 1102 { 1103 byte[] rawdata = decodeUrl( getAsciiBytes( escaped ) ); 1104 return Strings.getString( rawdata, "UTF-8" ); 1105 } 1106 catch ( UrlDecoderException e ) 1107 { 1108 throw new LdapUriException( e.getMessage(), e ); 1109 } 1110 } 1111 1112 1113 /** 1114 * Parse a string and check that it complies with RFC 2253. Here, we will 1115 * just call the Dn parser to do the job. 1116 * 1117 * @param chars The char array to be checked 1118 * @param pos the starting position 1119 * @return -1 if the char array does not contains a Dn 1120 */ 1121 private int parseDN( char[] chars, int pos ) 1122 { 1123 1124 int end = pos; 1125 1126 for ( int i = pos; ( i < chars.length ) && ( chars[i] != '?' ); i++ ) 1127 { 1128 end++; 1129 } 1130 1131 try 1132 { 1133 String dnStr = new String( chars, pos, end - pos ); 1134 dn = new Dn( decode( dnStr ) ); 1135 } 1136 catch ( LdapUriException | LdapInvalidDnException e ) 1137 { 1138 return -1; 1139 } 1140 1141 return end; 1142 } 1143 1144 1145 /** 1146 * Parse the following rule : 1147 * <pre> 1148 * oid ::= numericOid | descr 1149 * descr ::= keystring 1150 * keystring ::= leadkeychar *keychar 1151 * leadkeychar ::= [a-zA-Z] 1152 * keychar ::= [a-zA-Z0-0-] 1153 * numericOid ::= number 1*( DOT number ) 1154 * number ::= 0 | [1-9][0-9]* 1155 * 1156 * @param attribute 1157 * @throws LdapURLEncodingException 1158 */ 1159 private void validateAttribute( String attribute ) throws LdapURLEncodingException 1160 { 1161 Matcher matcher = ATTRIBUTE.matcher( attribute ); 1162 1163 if ( !matcher.matches() ) 1164 { 1165 throw new LdapURLEncodingException( "Attribute " + attribute + " is invalid" ); 1166 } 1167 } 1168 1169 1170 /** 1171 * Parse the attributes part 1172 * 1173 * @param chars The char array to be checked 1174 * @param pos the starting position 1175 * @return -1 if the char array does not contains attributes 1176 */ 1177 private int parseAttributes( char[] chars, int pos ) 1178 { 1179 int start = pos; 1180 int end = pos; 1181 Set<String> hAttributes = new HashSet<>(); 1182 boolean hadComma = false; 1183 1184 try 1185 { 1186 1187 for ( int i = pos; ( i < chars.length ) && ( chars[i] != '?' ); i++ ) 1188 { 1189 1190 if ( Chars.isCharASCII( chars, i, ',' ) ) 1191 { 1192 hadComma = true; 1193 1194 if ( ( end - start ) == 0 ) 1195 { 1196 1197 // An attributes must not be null 1198 return -1; 1199 } 1200 else 1201 { 1202 String attribute = null; 1203 1204 // get the attribute. It must not be blank 1205 attribute = new String( chars, start, end - start ).trim(); 1206 1207 if ( attribute.length() == 0 ) 1208 { 1209 return -1; 1210 } 1211 1212 // Check that the attribute is valid 1213 try 1214 { 1215 validateAttribute( attribute ); 1216 } 1217 catch ( LdapURLEncodingException luee ) 1218 { 1219 return -1; 1220 } 1221 1222 String decodedAttr = decode( attribute ); 1223 1224 if ( !hAttributes.contains( decodedAttr ) ) 1225 { 1226 attributes.add( decodedAttr ); 1227 hAttributes.add( decodedAttr ); 1228 } 1229 } 1230 1231 start = i + 1; 1232 } 1233 else 1234 { 1235 hadComma = false; 1236 } 1237 1238 end++; 1239 } 1240 1241 if ( hadComma ) 1242 { 1243 1244 // We are not allowed to have a comma at the end of the 1245 // attributes 1246 return -1; 1247 } 1248 else 1249 { 1250 1251 if ( end == start ) 1252 { 1253 1254 // We don't have any attributes. This is valid. 1255 return end; 1256 } 1257 1258 // Store the last attribute 1259 // get the attribute. It must not be blank 1260 String attribute = null; 1261 1262 attribute = new String( chars, start, end - start ).trim(); 1263 1264 if ( attribute.length() == 0 ) 1265 { 1266 return -1; 1267 } 1268 1269 String decodedAttr = decode( attribute ); 1270 1271 if ( !hAttributes.contains( decodedAttr ) ) 1272 { 1273 attributes.add( decodedAttr ); 1274 hAttributes.add( decodedAttr ); 1275 } 1276 } 1277 1278 return end; 1279 } 1280 catch ( LdapUriException ue ) 1281 { 1282 return -1; 1283 } 1284 } 1285 1286 1287 /** 1288 * Parse the filter part. We will use the FilterParserImpl class 1289 * 1290 * @param chars The char array to be checked 1291 * @param pos the starting position 1292 * @return -1 if the char array does not contains a filter 1293 */ 1294 private int parseFilter( char[] chars, int pos ) 1295 { 1296 1297 int end = pos; 1298 1299 for ( int i = pos; ( i < chars.length ) && ( chars[i] != '?' ); i++ ) 1300 { 1301 end++; 1302 } 1303 1304 if ( end == pos ) 1305 { 1306 // We have no filter 1307 return end; 1308 } 1309 1310 try 1311 { 1312 filter = decode( new String( chars, pos, end - pos ) ); 1313 FilterParser.parse( null, filter ); 1314 } 1315 catch ( LdapUriException | ParseException e ) 1316 { 1317 return -1; 1318 } 1319 1320 return end; 1321 } 1322 1323 1324 /** 1325 * Parse the scope part. 1326 * 1327 * @param chars The char array to be checked 1328 * @param pos the starting position 1329 * @return -1 if the char array does not contains a scope 1330 */ 1331 private int parseScope( char[] chars, int pos ) 1332 { 1333 1334 if ( Chars.isCharASCII( chars, pos, 'b' ) || Chars.isCharASCII( chars, pos, 'B' ) ) 1335 { 1336 pos++; 1337 1338 if ( Chars.isCharASCII( chars, pos, 'a' ) || Chars.isCharASCII( chars, pos, 'A' ) ) 1339 { 1340 pos++; 1341 1342 if ( Chars.isCharASCII( chars, pos, 's' ) || Chars.isCharASCII( chars, pos, 'S' ) ) 1343 { 1344 pos++; 1345 1346 if ( Chars.isCharASCII( chars, pos, 'e' ) || Chars.isCharASCII( chars, pos, 'E' ) ) 1347 { 1348 pos++; 1349 scope = SearchScope.OBJECT; 1350 return pos; 1351 } 1352 } 1353 } 1354 } 1355 else if ( Chars.isCharASCII( chars, pos, 'o' ) || Chars.isCharASCII( chars, pos, 'O' ) ) 1356 { 1357 pos++; 1358 1359 if ( Chars.isCharASCII( chars, pos, 'n' ) || Chars.isCharASCII( chars, pos, 'N' ) ) 1360 { 1361 pos++; 1362 1363 if ( Chars.isCharASCII( chars, pos, 'e' ) || Chars.isCharASCII( chars, pos, 'E' ) ) 1364 { 1365 pos++; 1366 1367 scope = SearchScope.ONELEVEL; 1368 return pos; 1369 } 1370 } 1371 } 1372 else if ( Chars.isCharASCII( chars, pos, 's' ) || Chars.isCharASCII( chars, pos, 'S' ) ) 1373 { 1374 pos++; 1375 1376 if ( Chars.isCharASCII( chars, pos, 'u' ) || Chars.isCharASCII( chars, pos, 'U' ) ) 1377 { 1378 pos++; 1379 1380 if ( Chars.isCharASCII( chars, pos, 'b' ) || Chars.isCharASCII( chars, pos, 'B' ) ) 1381 { 1382 pos++; 1383 1384 scope = SearchScope.SUBTREE; 1385 return pos; 1386 } 1387 } 1388 } 1389 else if ( Chars.isCharASCII( chars, pos, '?' ) ) 1390 { 1391 // An empty scope. This is valid 1392 return pos; 1393 } 1394 else if ( pos == chars.length ) 1395 { 1396 // An empty scope at the end of the URL. This is valid 1397 return pos; 1398 } 1399 1400 // The scope is not one of "one", "sub" or "base". It's an error 1401 return -1; 1402 } 1403 1404 1405 /** 1406 * Parse extensions and critical extensions. 1407 * 1408 * The grammar is : 1409 * extensions ::= extension [ ',' extension ]* 1410 * extension ::= [ '!' ] ( token | ( 'x-' | 'X-' ) token ) ) [ '=' exvalue ] 1411 * 1412 * @param chars The char array to be checked 1413 * @param pos the starting position 1414 * @return -1 if the char array does not contains valid extensions or 1415 * critical extensions 1416 */ 1417 private int parseExtensions( char[] chars, int pos ) 1418 { 1419 int start = pos; 1420 boolean isCritical = false; 1421 boolean isNewExtension = true; 1422 boolean hasValue = false; 1423 String extension = null; 1424 String value = null; 1425 1426 if ( pos == chars.length ) 1427 { 1428 return pos; 1429 } 1430 1431 try 1432 { 1433 for ( int i = pos; i < chars.length; i++ ) 1434 { 1435 if ( Chars.isCharASCII( chars, i, ',' ) ) 1436 { 1437 if ( isNewExtension ) 1438 { 1439 // a ',' is not allowed when we have already had one 1440 // or if we just started to parse the extensions. 1441 return -1; 1442 } 1443 else 1444 { 1445 if ( extension == null ) 1446 { 1447 extension = decode( new String( chars, start, i - start ) ).trim(); 1448 } 1449 else 1450 { 1451 value = decode( new String( chars, start, i - start ) ).trim(); 1452 } 1453 1454 Extension ext = new Extension( isCritical, extension, value ); 1455 extensionList.add( ext ); 1456 1457 isNewExtension = true; 1458 hasValue = false; 1459 isCritical = false; 1460 start = i + 1; 1461 extension = null; 1462 value = null; 1463 } 1464 } 1465 else if ( Chars.isCharASCII( chars, i, '=' ) ) 1466 { 1467 if ( hasValue ) 1468 { 1469 // We may have two '=' for the same extension 1470 continue; 1471 } 1472 1473 // An optionnal value 1474 extension = decode( new String( chars, start, i - start ) ).trim(); 1475 1476 if ( extension.length() == 0 ) 1477 { 1478 // We must have an extension 1479 return -1; 1480 } 1481 1482 hasValue = true; 1483 start = i + 1; 1484 } 1485 else if ( Chars.isCharASCII( chars, i, '!' ) ) 1486 { 1487 if ( hasValue ) 1488 { 1489 // We may have two '!' in the value 1490 continue; 1491 } 1492 1493 if ( !isNewExtension ) 1494 { 1495 // '!' must appears first 1496 return -1; 1497 } 1498 1499 isCritical = true; 1500 start++; 1501 } 1502 else 1503 { 1504 isNewExtension = false; 1505 } 1506 } 1507 1508 if ( extension == null ) 1509 { 1510 extension = decode( new String( chars, start, chars.length - start ) ).trim(); 1511 } 1512 else 1513 { 1514 value = decode( new String( chars, start, chars.length - start ) ).trim(); 1515 } 1516 1517 Extension ext = new Extension( isCritical, extension, value ); 1518 extensionList.add( ext ); 1519 1520 return chars.length; 1521 } 1522 catch ( LdapUriException ue ) 1523 { 1524 return -1; 1525 } 1526 } 1527 1528 1529 /** 1530 * Encode a String to avoid special characters. 1531 * 1532 * <pre> 1533 * RFC 4516, section 2.1. (Percent-Encoding) 1534 * 1535 * A generated LDAP URL MUST consist only of the restricted set of 1536 * characters included in one of the following three productions defined 1537 * in [RFC3986]: 1538 * 1539 * <reserved> 1540 * <unreserved> 1541 * <pct-encoded> 1542 * 1543 * Implementations SHOULD accept other valid UTF-8 strings [RFC3629] as 1544 * input. An octet MUST be encoded using the percent-encoding mechanism 1545 * described in section 2.1 of [RFC3986] in any of these situations: 1546 * 1547 * The octet is not in the reserved set defined in section 2.2 of 1548 * [RFC3986] or in the unreserved set defined in section 2.3 of 1549 * [RFC3986]. 1550 * 1551 * It is the single Reserved character '?' and occurs inside a <dn>, 1552 * <filter>, or other element of an LDAP URL. 1553 * 1554 * It is a comma character ',' that occurs inside an <exvalue>. 1555 * 1556 * RFC 3986, section 2.2 (Reserved Characters) 1557 * 1558 * reserved = gen-delims / sub-delims 1559 * gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" 1560 * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 1561 * / "*" / "+" / "," / ";" / "=" 1562 * 1563 * RFC 3986, section 2.3 (Unreserved Characters) 1564 * 1565 * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 1566 * </pre> 1567 * 1568 * @param url The String to encode 1569 * @param doubleEncode Set if we need to encode the comma 1570 * @return An encoded string 1571 */ 1572 public static String urlEncode( String url, boolean doubleEncode ) 1573 { 1574 StringBuffer sb = new StringBuffer(); 1575 1576 for ( int i = 0; i < url.length(); i++ ) 1577 { 1578 char c = url.charAt( i ); 1579 1580 switch ( c ) 1581 1582 { 1583 // reserved and unreserved characters: 1584 // just append to the buffer 1585 1586 // reserved gen-delims, excluding '?' 1587 // gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" 1588 case ':': 1589 case '/': 1590 case '#': 1591 case '[': 1592 case ']': 1593 case '@': 1594 1595 // reserved sub-delims, excluding ',' 1596 // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 1597 // / "*" / "+" / "," / ";" / "=" 1598 case '!': 1599 case '$': 1600 case '&': 1601 case '\'': 1602 case '(': 1603 case ')': 1604 case '*': 1605 case '+': 1606 case ';': 1607 case '=': 1608 1609 // unreserved 1610 // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 1611 case 'a': 1612 case 'b': 1613 case 'c': 1614 case 'd': 1615 case 'e': 1616 case 'f': 1617 case 'g': 1618 case 'h': 1619 case 'i': 1620 case 'j': 1621 case 'k': 1622 case 'l': 1623 case 'm': 1624 case 'n': 1625 case 'o': 1626 case 'p': 1627 case 'q': 1628 case 'r': 1629 case 's': 1630 case 't': 1631 case 'u': 1632 case 'v': 1633 case 'w': 1634 case 'x': 1635 case 'y': 1636 case 'z': 1637 1638 case 'A': 1639 case 'B': 1640 case 'C': 1641 case 'D': 1642 case 'E': 1643 case 'F': 1644 case 'G': 1645 case 'H': 1646 case 'I': 1647 case 'J': 1648 case 'K': 1649 case 'L': 1650 case 'M': 1651 case 'N': 1652 case 'O': 1653 case 'P': 1654 case 'Q': 1655 case 'R': 1656 case 'S': 1657 case 'T': 1658 case 'U': 1659 case 'V': 1660 case 'W': 1661 case 'X': 1662 case 'Y': 1663 case 'Z': 1664 1665 case '0': 1666 case '1': 1667 case '2': 1668 case '3': 1669 case '4': 1670 case '5': 1671 case '6': 1672 case '7': 1673 case '8': 1674 case '9': 1675 1676 case '-': 1677 case '.': 1678 case '_': 1679 case '~': 1680 1681 sb.append( c ); 1682 break; 1683 1684 case ',': 1685 1686 // special case for comma 1687 if ( doubleEncode ) 1688 { 1689 sb.append( "%2c" ); 1690 } 1691 else 1692 { 1693 sb.append( c ); 1694 } 1695 break; 1696 1697 default: 1698 1699 // percent encoding 1700 byte[] bytes = Unicode.charToBytes( c ); 1701 char[] hex = Strings.toHexString( bytes ).toCharArray(); 1702 for ( int j = 0; j < hex.length; j++ ) 1703 { 1704 if ( j % 2 == 0 ) 1705 { 1706 sb.append( '%' ); 1707 } 1708 sb.append( hex[j] ); 1709 1710 } 1711 1712 break; 1713 } 1714 } 1715 1716 return sb.toString(); 1717 } 1718 1719 1720 /** 1721 * Get a string representation of a LdapUrl. 1722 * 1723 * @return A LdapUrl string 1724 */ 1725 @Override 1726 public String toString() 1727 { 1728 StringBuffer sb = new StringBuffer(); 1729 1730 sb.append( scheme ); 1731 1732 if ( host != null ) 1733 { 1734 switch ( hostType ) 1735 { 1736 case IPV4: 1737 case REGULAR_NAME: 1738 sb.append( host ); 1739 break; 1740 1741 case IPV6: 1742 case IPV_FUTURE: 1743 sb.append( '[' ).append( host ).append( ']' ); 1744 break; 1745 1746 default: 1747 throw new IllegalArgumentException( "Unexpected HostTypeEnum " + hostType ); 1748 } 1749 } 1750 1751 if ( port != -1 ) 1752 { 1753 sb.append( ':' ).append( port ); 1754 } 1755 1756 if ( dn != null ) 1757 { 1758 sb.append( '/' ).append( urlEncode( dn.getName(), false ) ); 1759 1760 if ( !attributes.isEmpty() || forceScopeRendering 1761 || ( ( scope != SearchScope.OBJECT ) || ( filter != null ) || !extensionList.isEmpty() ) ) 1762 { 1763 sb.append( '?' ); 1764 1765 boolean isFirst = true; 1766 1767 for ( String attribute : attributes ) 1768 { 1769 if ( isFirst ) 1770 { 1771 isFirst = false; 1772 } 1773 else 1774 { 1775 sb.append( ',' ); 1776 } 1777 1778 sb.append( urlEncode( attribute, false ) ); 1779 } 1780 } 1781 1782 if ( forceScopeRendering ) 1783 { 1784 sb.append( '?' ); 1785 1786 sb.append( scope.getLdapUrlValue() ); 1787 } 1788 else 1789 { 1790 if ( ( scope != SearchScope.OBJECT ) || ( filter != null ) || !extensionList.isEmpty() ) 1791 { 1792 sb.append( '?' ); 1793 1794 switch ( scope ) 1795 { 1796 case ONELEVEL: 1797 case SUBTREE: 1798 sb.append( scope.getLdapUrlValue() ); 1799 break; 1800 1801 default: 1802 break; 1803 } 1804 1805 if ( ( filter != null ) || !extensionList.isEmpty() ) 1806 { 1807 sb.append( "?" ); 1808 1809 if ( filter != null ) 1810 { 1811 sb.append( urlEncode( filter, false ) ); 1812 } 1813 1814 if ( !extensionList.isEmpty() ) 1815 { 1816 sb.append( '?' ); 1817 1818 boolean isFirst = true; 1819 1820 if ( !extensionList.isEmpty() ) 1821 { 1822 for ( Extension extension : extensionList ) 1823 { 1824 if ( !isFirst ) 1825 { 1826 sb.append( ',' ); 1827 } 1828 else 1829 { 1830 isFirst = false; 1831 } 1832 1833 if ( extension.isCritical ) 1834 { 1835 sb.append( '!' ); 1836 } 1837 sb.append( urlEncode( extension.type, false ) ); 1838 1839 if ( extension.value != null ) 1840 { 1841 sb.append( '=' ); 1842 sb.append( urlEncode( extension.value, true ) ); 1843 } 1844 } 1845 } 1846 } 1847 } 1848 } 1849 } 1850 } 1851 else 1852 { 1853 sb.append( '/' ); 1854 } 1855 1856 return sb.toString(); 1857 } 1858 1859 1860 /** 1861 * @return Returns the attributes. 1862 */ 1863 public List<String> getAttributes() 1864 { 1865 return attributes; 1866 } 1867 1868 1869 /** 1870 * @return Returns the dn. 1871 */ 1872 public Dn getDn() 1873 { 1874 return dn; 1875 } 1876 1877 1878 /** 1879 * @return Returns the extensions. 1880 */ 1881 public List<Extension> getExtensions() 1882 { 1883 return extensionList; 1884 } 1885 1886 1887 /** 1888 * Gets the extension. 1889 * 1890 * @param type the extension type, case-insensitive 1891 * 1892 * @return Returns the extension, null if this URL does not contain 1893 * such an extension. 1894 */ 1895 public Extension getExtension( String type ) 1896 { 1897 for ( Extension extension : getExtensions() ) 1898 { 1899 if ( extension.getType().equalsIgnoreCase( type ) ) 1900 { 1901 return extension; 1902 } 1903 } 1904 return null; 1905 } 1906 1907 1908 /** 1909 * Gets the extension value. 1910 * 1911 * @param type the extension type, case-insensitive 1912 * 1913 * @return Returns the extension value, null if this URL does not 1914 * contain such an extension or if the extension value is null. 1915 */ 1916 public String getExtensionValue( String type ) 1917 { 1918 for ( Extension extension : getExtensions() ) 1919 { 1920 if ( extension.getType().equalsIgnoreCase( type ) ) 1921 { 1922 return extension.getValue(); 1923 } 1924 } 1925 return null; 1926 } 1927 1928 1929 /** 1930 * @return Returns the filter. 1931 */ 1932 public String getFilter() 1933 { 1934 return filter; 1935 } 1936 1937 1938 /** 1939 * @return Returns the host. 1940 */ 1941 public String getHost() 1942 { 1943 return host; 1944 } 1945 1946 1947 /** 1948 * @return Returns the port. 1949 */ 1950 public int getPort() 1951 { 1952 return port; 1953 } 1954 1955 1956 /** 1957 * Returns the scope, one of {@link SearchScope#OBJECT}, 1958 * {@link SearchScope#ONELEVEL} or {@link SearchScope#SUBTREE}. 1959 * 1960 * @return Returns the scope. 1961 */ 1962 public SearchScope getScope() 1963 { 1964 return scope; 1965 } 1966 1967 1968 /** 1969 * @return Returns the scheme. 1970 */ 1971 public String getScheme() 1972 { 1973 return scheme; 1974 } 1975 1976 1977 /** 1978 * @return the number of bytes for this LdapUrl 1979 */ 1980 public int getNbBytes() 1981 { 1982 return bytes != null ? bytes.length : 0; 1983 } 1984 1985 1986 /** 1987 * @return a reference on the interned bytes representing this LdapUrl 1988 */ 1989 public byte[] getBytesReference() 1990 { 1991 return bytes; 1992 } 1993 1994 1995 /** 1996 * @return a copy of the bytes representing this LdapUrl 1997 */ 1998 public byte[] getBytesCopy() 1999 { 2000 if ( bytes != null ) 2001 { 2002 byte[] copy = new byte[bytes.length]; 2003 System.arraycopy( bytes, 0, copy, 0, bytes.length ); 2004 return copy; 2005 } 2006 else 2007 { 2008 return null; 2009 } 2010 } 2011 2012 2013 /** 2014 * @return the LdapUrl as a String 2015 */ 2016 public String getString() 2017 { 2018 return string; 2019 } 2020 2021 2022 /** 2023 * {@inheritDoc} 2024 */ 2025 @Override 2026 public int hashCode() 2027 { 2028 return this.toString().hashCode(); 2029 } 2030 2031 2032 /** 2033 * {@inheritDoc} 2034 */ 2035 @Override 2036 public boolean equals( Object obj ) 2037 { 2038 if ( this == obj ) 2039 { 2040 return true; 2041 } 2042 if ( obj == null ) 2043 { 2044 return false; 2045 } 2046 if ( getClass() != obj.getClass() ) 2047 { 2048 return false; 2049 } 2050 2051 final LdapUrl other = ( LdapUrl ) obj; 2052 return this.toString().equals( other.toString() ); 2053 } 2054 2055 2056 /** 2057 * Sets the scheme. Must be "ldap://" or "ldaps://", otherwise "ldap://" is assumed as default. 2058 * 2059 * @param scheme the new scheme 2060 */ 2061 public void setScheme( String scheme ) 2062 { 2063 if ( ( ( scheme != null ) && LDAP_SCHEME.equals( scheme ) ) || LDAPS_SCHEME.equals( scheme ) ) 2064 { 2065 this.scheme = scheme; 2066 } 2067 else 2068 { 2069 this.scheme = LDAP_SCHEME; 2070 } 2071 2072 } 2073 2074 2075 /** 2076 * Sets the host. 2077 * 2078 * @param host the new host 2079 */ 2080 public void setHost( String host ) 2081 { 2082 this.host = host; 2083 } 2084 2085 2086 /** 2087 * Sets the port. Must be between 1 and 65535, otherwise -1 is assumed as default. 2088 * 2089 * @param port the new port 2090 */ 2091 public void setPort( int port ) 2092 { 2093 if ( ( port < 1 ) || ( port > 65535 ) ) 2094 { 2095 this.port = -1; 2096 } 2097 else 2098 { 2099 this.port = port; 2100 } 2101 } 2102 2103 2104 /** 2105 * Sets the dn. 2106 * 2107 * @param dn the new dn 2108 */ 2109 public void setDn( Dn dn ) 2110 { 2111 this.dn = dn; 2112 } 2113 2114 2115 /** 2116 * Sets the attributes, null removes all existing attributes. 2117 * 2118 * @param attributes the new attributes 2119 */ 2120 public void setAttributes( List<String> attributes ) 2121 { 2122 if ( attributes == null ) 2123 { 2124 this.attributes.clear(); 2125 } 2126 else 2127 { 2128 this.attributes = attributes; 2129 } 2130 } 2131 2132 2133 /** 2134 * Sets the scope. Must be one of {@link SearchScope#OBJECT}, 2135 * {@link SearchScope#ONELEVEL} or {@link SearchScope#SUBTREE}, 2136 * otherwise {@link SearchScope#OBJECT} is assumed as default. 2137 * 2138 * @param scope the new scope 2139 */ 2140 public void setScope( int scope ) 2141 { 2142 try 2143 { 2144 this.scope = SearchScope.getSearchScope( scope ); 2145 } 2146 catch ( IllegalArgumentException iae ) 2147 { 2148 this.scope = SearchScope.OBJECT; 2149 } 2150 } 2151 2152 2153 /** 2154 * Sets the scope. Must be one of {@link SearchScope#OBJECT}, 2155 * {@link SearchScope#ONELEVEL} or {@link SearchScope#SUBTREE}, 2156 * otherwise {@link SearchScope#OBJECT} is assumed as default. 2157 * 2158 * @param scope the new scope 2159 */ 2160 public void setScope( SearchScope scope ) 2161 { 2162 if ( scope == null ) 2163 { 2164 this.scope = SearchScope.OBJECT; 2165 } 2166 else 2167 { 2168 this.scope = scope; 2169 } 2170 } 2171 2172 2173 /** 2174 * Sets the filter. 2175 * 2176 * @param filter the new filter 2177 */ 2178 public void setFilter( String filter ) 2179 { 2180 this.filter = filter; 2181 } 2182 2183 2184 /** 2185 * If set to true forces the toString method to render the scope 2186 * regardless of optional nature. Use this when you want explicit 2187 * search URL scope rendering. 2188 * 2189 * @param forceScopeRendering the forceScopeRendering to set 2190 */ 2191 public void setForceScopeRendering( boolean forceScopeRendering ) 2192 { 2193 this.forceScopeRendering = forceScopeRendering; 2194 } 2195 2196 /** 2197 * An inner bean to hold extension information. 2198 * 2199 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a> 2200 */ 2201 public static class Extension 2202 { 2203 private boolean isCritical; 2204 private String type; 2205 private String value; 2206 2207 2208 /** 2209 * Creates a new instance of Extension. 2210 * 2211 * @param isCritical true for critical extension 2212 * @param type the extension type 2213 * @param value the extension value 2214 */ 2215 public Extension( boolean isCritical, String type, String value ) 2216 { 2217 super(); 2218 this.isCritical = isCritical; 2219 this.type = type; 2220 this.value = value; 2221 } 2222 2223 2224 /** 2225 * Checks if is critical. 2226 * 2227 * @return true, if is critical 2228 */ 2229 public boolean isCritical() 2230 { 2231 return isCritical; 2232 } 2233 2234 2235 /** 2236 * Sets the critical flag. 2237 * 2238 * @param critical the new critical flag 2239 */ 2240 public void setCritical( boolean critical ) 2241 { 2242 this.isCritical = critical; 2243 } 2244 2245 2246 /** 2247 * Gets the type. 2248 * 2249 * @return the type 2250 */ 2251 public String getType() 2252 { 2253 return type; 2254 } 2255 2256 2257 /** 2258 * Sets the type. 2259 * 2260 * @param type the new type 2261 */ 2262 public void setType( String type ) 2263 { 2264 this.type = type; 2265 } 2266 2267 2268 /** 2269 * Gets the value. 2270 * 2271 * @return the value 2272 */ 2273 public String getValue() 2274 { 2275 return value; 2276 } 2277 2278 2279 /** 2280 * Sets the value. 2281 * 2282 * @param value the new value 2283 */ 2284 public void setValue( String value ) 2285 { 2286 this.value = value; 2287 } 2288 } 2289}