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