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