001/* 002 * CDDL HEADER START 003 * 004 * The contents of this file are subject to the terms of the 005 * Common Development and Distribution License, Version 1.0 only 006 * (the "License"). You may not use this file except in compliance 007 * with the License. 008 * 009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt 010 * or http://forgerock.org/license/CDDLv1.0.html. 011 * See the License for the specific language governing permissions 012 * and limitations under the License. 013 * 014 * When distributing Covered Code, include this CDDL HEADER in each 015 * file and include the License file at legal-notices/CDDLv1_0.txt. 016 * If applicable, add the following below this CDDL HEADER, with the 017 * fields enclosed by brackets "[]" replaced with your own identifying 018 * information: 019 * Portions Copyright [yyyy] [name of copyright owner] 020 * 021 * CDDL HEADER END 022 * 023 * 024 * Copyright 2010 Sun Microsystems, Inc. 025 * Portions copyright 2012-2015 ForgeRock AS. 026 */ 027package org.forgerock.opendj.ldap; 028 029import java.util.ArrayList; 030import java.util.Arrays; 031import java.util.Collections; 032import java.util.HashSet; 033import java.util.List; 034import java.util.Set; 035import java.util.StringTokenizer; 036 037import org.forgerock.i18n.LocalizableMessage; 038import org.forgerock.i18n.LocalizedIllegalArgumentException; 039import org.forgerock.opendj.ldap.requests.Requests; 040import org.forgerock.opendj.ldap.requests.SearchRequest; 041import org.forgerock.opendj.ldap.schema.Schema; 042import org.forgerock.util.Reject; 043 044import com.forgerock.opendj.util.StaticUtils; 045 046import static com.forgerock.opendj.ldap.CoreMessages.*; 047import static com.forgerock.opendj.util.StaticUtils.*; 048 049/** 050 * An LDAP URL as defined in RFC 4516. In addition, the secure ldap (ldaps://) 051 * is also supported. LDAP URLs have the following format: 052 * 053 * <PRE> 054 * "ldap[s]://" [ <I>hostName</I> [":" <I>portNumber</I>] ] 055 * "/" <I>distinguishedName</I> 056 * ["?" <I>attributeList</I> 057 * ["?" <I>scope</I> "?" <I>filterString</I> ] ] 058 * </PRE> 059 * 060 * Where: 061 * <UL> 062 * <LI>all text within double-quotes are literal 063 * <LI><CODE><I>hostName</I></CODE> and <CODE><I>portNumber</I></CODE> identify 064 * the location of the LDAP server. 065 * <LI><CODE><I>distinguishedName</I></CODE> is the name of an entry within the 066 * given directory (the entry represents the starting point of the search). 067 * <LI><CODE><I>attributeList</I></CODE> contains a list of attributes to 068 * retrieve (if null, fetch all attributes). This is a comma-delimited list of 069 * attribute names. 070 * <LI><CODE><I>scope</I></CODE> is one of the following: 071 * <UL> 072 * <LI><CODE>base</CODE> indicates that this is a search only for the specified 073 * entry 074 * <LI><CODE>one</CODE> indicates that this is a search for matching entries one 075 * level under the specified entry (and not including the entry itself) 076 * <LI><CODE>sub</CODE> indicates that this is a search for matching entries at 077 * all levels under the specified entry (including the entry itself) 078 * <LI><CODE>subordinates</CODE> indicates that this is a search for matching 079 * entries all levels under the specified entry (excluding the entry itself) 080 * </UL> 081 * If not specified, <CODE><I>scope</I></CODE> is <CODE>base</CODE> by default. 082 * <LI><CODE><I>filterString</I></CODE> is a human-readable representation of 083 * the search criteria. If no filter is provided, then a default of " 084 * {@code (objectClass=*)}" should be assumed. 085 * </UL> 086 * The same encoding rules for other URLs (e.g. HTTP) apply for LDAP URLs. 087 * Specifically, any "illegal" characters are escaped with 088 * <CODE>%<I>HH</I></CODE>, where <CODE><I>HH</I></CODE> represent the two hex 089 * digits which correspond to the ASCII value of the character. This encoding is 090 * only legal (or necessary) on the DN and filter portions of the URL. 091 * <P> 092 * Note that this class does not implement extensions. 093 * 094 * @see <a href="http://www.ietf.org/rfc/rfc4516">RFC 4516 - Lightweight 095 * Directory Access Protocol (LDAP): Uniform Resource Locator</a> 096 */ 097public final class LDAPUrl { 098 /** 099 * The scheme corresponding to an LDAP URL. RFC 4516 mandates only ldap 100 * scheme but we support "ldaps" too. 101 */ 102 private final boolean isSecured; 103 104 /** 105 * The host name corresponding to an LDAP URL. 106 */ 107 private final String host; 108 109 /** 110 * The port number corresponding to an LDAP URL. 111 */ 112 private final int port; 113 114 /** 115 * The distinguished name corresponding to an LDAP URL. 116 */ 117 private final DN name; 118 119 /** 120 * The search scope corresponding to an LDAP URL. 121 */ 122 private final SearchScope scope; 123 124 /** 125 * The search filter corresponding to an LDAP URL. 126 */ 127 private final Filter filter; 128 129 /** 130 * The attributes that need to be searched. 131 */ 132 private final List<String> attributes; 133 134 /** 135 * The String value of LDAP URL. 136 */ 137 private final String urlString; 138 139 /** 140 * Normalized ldap URL. 141 */ 142 private String normalizedURL; 143 144 /** 145 * The default scheme to be used with LDAP URL. 146 */ 147 private static final String DEFAULT_URL_SCHEME = "ldap"; 148 149 /** 150 * The SSL-based scheme allowed to be used with LDAP URL. 151 */ 152 private static final String SSL_URL_SCHEME = "ldaps"; 153 154 /** 155 * The default host. 156 */ 157 private static final String DEFAULT_HOST = "localhost"; 158 159 /** 160 * The default non-SSL port. 161 */ 162 private static final int DEFAULT_PORT = 389; 163 164 /** 165 * The default SSL port. 166 */ 167 private static final int DEFAULT_SSL_PORT = 636; 168 169 /** 170 * The default filter. 171 */ 172 private static final Filter DEFAULT_FILTER = Filter.objectClassPresent(); 173 174 /** 175 * The default search scope. 176 */ 177 private static final SearchScope DEFAULT_SCOPE = SearchScope.BASE_OBJECT; 178 179 /** 180 * The default distinguished name. 181 */ 182 private static final DN DEFAULT_DN = DN.rootDN(); 183 184 /** 185 * The % encoding character. 186 */ 187 private static final char PERCENT_ENCODING_CHAR = '%'; 188 189 /** 190 * The ? character. 191 */ 192 private static final char QUESTION_CHAR = '?'; 193 194 /** 195 * The slash (/) character. 196 */ 197 private static final char SLASH_CHAR = '/'; 198 199 /** 200 * The comma (,) character. 201 */ 202 private static final char COMMA_CHAR = ','; 203 204 /** 205 * The colon (:) character. 206 */ 207 private static final char COLON_CHAR = ':'; 208 209 /** 210 * Set containing characters that do not need to be encoded. 211 */ 212 private static final Set<Character> VALID_CHARS = new HashSet<>(); 213 214 static { 215 // Refer to RFC 3986 for more details. 216 final char[] delims = { 217 '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', '.', '-', '_', '~' 218 }; 219 for (final char c : delims) { 220 VALID_CHARS.add(c); 221 } 222 223 for (char c = 'a'; c <= 'z'; c++) { 224 VALID_CHARS.add(c); 225 } 226 227 for (char c = 'A'; c <= 'Z'; c++) { 228 VALID_CHARS.add(c); 229 } 230 231 for (char c = '0'; c <= '9'; c++) { 232 VALID_CHARS.add(c); 233 } 234 } 235 236 /** 237 * Parses the provided LDAP string representation of an LDAP URL using the 238 * default schema. 239 * 240 * @param url 241 * The LDAP string representation of an LDAP URL. 242 * @return The parsed LDAP URL. 243 * @throws LocalizedIllegalArgumentException 244 * If {@code url} is not a valid LDAP string representation of 245 * an LDAP URL. 246 * @throws NullPointerException 247 * If {@code url} was {@code null}. 248 */ 249 public static LDAPUrl valueOf(final String url) { 250 return valueOf(url, Schema.getDefaultSchema()); 251 } 252 253 /** 254 * Parses the provided LDAP string representation of an LDAP URL using the 255 * provided schema. 256 * 257 * @param url 258 * The LDAP string representation of an LDAP URL. 259 * @param schema 260 * The schema to use when parsing the LDAP URL. 261 * @return The parsed LDAP URL. 262 * @throws LocalizedIllegalArgumentException 263 * If {@code url} is not a valid LDAP string representation of 264 * an LDAP URL. 265 * @throws NullPointerException 266 * If {@code url} or {@code schema} was {@code null}. 267 */ 268 public static LDAPUrl valueOf(final String url, final Schema schema) { 269 Reject.ifNull(url, schema); 270 return new LDAPUrl(url, schema); 271 } 272 273 private static int decodeHex(final String url, final int index, final char hexChar) { 274 if (hexChar >= '0' && hexChar <= '9') { 275 return hexChar - '0'; 276 } else if (hexChar >= 'A' && hexChar <= 'F') { 277 return hexChar - 'A' + 10; 278 } else if (hexChar >= 'a' && hexChar <= 'f') { 279 return hexChar - 'a' + 10; 280 } 281 282 final LocalizableMessage msg = ERR_LDAPURL_INVALID_HEX_BYTE.get(url, index); 283 throw new LocalizedIllegalArgumentException(msg); 284 } 285 286 private static void percentDecoder(final String urlString, final int index, final String s, 287 final StringBuilder decoded) { 288 Reject.ifNull(s); 289 Reject.ifNull(decoded); 290 decoded.append(s); 291 292 int srcPos = 0, dstPos = 0; 293 294 while (srcPos < decoded.length()) { 295 if (decoded.charAt(srcPos) != '%') { 296 if (srcPos != dstPos) { 297 decoded.setCharAt(dstPos, decoded.charAt(srcPos)); 298 } 299 srcPos++; 300 dstPos++; 301 continue; 302 } 303 int i = decodeHex(urlString, index + srcPos + 1, decoded.charAt(srcPos + 1)) << 4; 304 int j = decodeHex(urlString, index + srcPos + 2, decoded.charAt(srcPos + 2)); 305 decoded.setCharAt(dstPos, (char) (i | j)); 306 dstPos++; 307 srcPos += 3; 308 } 309 decoded.setLength(dstPos); 310 } 311 312 /** 313 * This method performs the percent-encoding as defined in section 2.1 of 314 * RFC 3986. 315 * 316 * @param urlElement 317 * The element of the URL that needs to be percent encoded. 318 * @param encodedBuffer 319 * The buffer that contains the final percent encoded value. 320 */ 321 private static void percentEncoder(final String urlElement, final StringBuilder encodedBuffer) { 322 Reject.ifNull(urlElement); 323 for (int count = 0; count < urlElement.length(); count++) { 324 final char c = urlElement.charAt(count); 325 if (VALID_CHARS.contains(c)) { 326 encodedBuffer.append(c); 327 } else { 328 encodedBuffer.append(PERCENT_ENCODING_CHAR); 329 encodedBuffer.append(Integer.toHexString(c)); 330 } 331 } 332 } 333 334 /** 335 * Creates a new LDAP URL referring to a single entry on the specified 336 * server. The LDAP URL with have base object scope and the filter 337 * {@code (objectClass=*)}. 338 * 339 * @param isSecured 340 * {@code true} if this LDAP URL should use LDAPS or 341 * {@code false} if it should use LDAP. 342 * @param host 343 * The name or IP address in dotted format of the LDAP server. 344 * For example, {@code ldap.server1.com} or 345 * {@code 192.202.185.90}. Use {@code null} for the local host. 346 * @param port 347 * The port number of the LDAP server, or {@code null} to use the 348 * default port (389 for LDAP and 636 for LDAPS). 349 * @param name 350 * The distinguished name of the base entry relative to which the 351 * search is to be performed, or {@code null} to specify the root 352 * DSE. 353 * @throws LocalizedIllegalArgumentException 354 * If {@code port} was less than 1 or greater than 65535. 355 */ 356 public LDAPUrl(final boolean isSecured, final String host, final Integer port, final DN name) { 357 this(isSecured, host, port, name, DEFAULT_SCOPE, DEFAULT_FILTER); 358 } 359 360 /** 361 * Creates a new LDAP URL including the full set of parameters for a search 362 * request. 363 * 364 * @param isSecured 365 * {@code true} if this LDAP URL should use LDAPS or 366 * {@code false} if it should use LDAP. 367 * @param host 368 * The name or IP address in dotted format of the LDAP server. 369 * For example, {@code ldap.server1.com} or 370 * {@code 192.202.185.90}. Use {@code null} for the local host. 371 * @param port 372 * The port number of the LDAP server, or {@code null} to use the 373 * default port (389 for LDAP and 636 for LDAPS). 374 * @param name 375 * The distinguished name of the base entry relative to which the 376 * search is to be performed, or {@code null} to specify the root 377 * DSE. 378 * @param scope 379 * The search scope, or {@code null} to specify base scope. 380 * @param filter 381 * The search filter, or {@code null} to specify the filter 382 * {@code (objectClass=*)}. 383 * @param attributes 384 * The list of attributes to be included in the search results. 385 * @throws LocalizedIllegalArgumentException 386 * If {@code port} was less than 1 or greater than 65535. 387 */ 388 public LDAPUrl(final boolean isSecured, final String host, final Integer port, final DN name, 389 final SearchScope scope, final Filter filter, final String... attributes) { 390 // The buffer storing the encoded url. 391 final StringBuilder urlBuffer = new StringBuilder(); 392 393 // build the scheme. 394 this.isSecured = isSecured; 395 if (this.isSecured) { 396 urlBuffer.append(SSL_URL_SCHEME); 397 } else { 398 urlBuffer.append(DEFAULT_URL_SCHEME); 399 } 400 urlBuffer.append("://"); 401 402 if (host == null) { 403 this.host = DEFAULT_HOST; 404 } else { 405 this.host = host; 406 urlBuffer.append(this.host); 407 } 408 409 int listenPort = DEFAULT_PORT; 410 if (port == null) { 411 listenPort = isSecured ? DEFAULT_SSL_PORT : DEFAULT_PORT; 412 } else { 413 listenPort = port.intValue(); 414 if (listenPort < 1 || listenPort > 65535) { 415 final LocalizableMessage msg = ERR_LDAPURL_BAD_PORT.get(listenPort); 416 throw new LocalizedIllegalArgumentException(msg); 417 } 418 urlBuffer.append(COLON_CHAR); 419 urlBuffer.append(listenPort); 420 } 421 422 this.port = listenPort; 423 424 // We need a slash irrespective of dn is defined or not. 425 urlBuffer.append(SLASH_CHAR); 426 if (name != null) { 427 this.name = name; 428 percentEncoder(name.toString(), urlBuffer); 429 } else { 430 this.name = DEFAULT_DN; 431 } 432 433 // Add attributes. 434 urlBuffer.append(QUESTION_CHAR); 435 switch (attributes.length) { 436 case 0: 437 this.attributes = Collections.emptyList(); 438 break; 439 case 1: 440 this.attributes = Collections.singletonList(attributes[0]); 441 urlBuffer.append(attributes[0]); 442 break; 443 default: 444 this.attributes = Collections.unmodifiableList(Arrays.asList(attributes)); 445 urlBuffer.append(attributes[0]); 446 for (int i = 1; i < attributes.length; i++) { 447 urlBuffer.append(COMMA_CHAR); 448 urlBuffer.append(attributes[i]); 449 } 450 break; 451 } 452 453 // Add the scope. 454 urlBuffer.append(QUESTION_CHAR); 455 if (scope != null) { 456 this.scope = scope; 457 urlBuffer.append(scope); 458 } else { 459 this.scope = DEFAULT_SCOPE; 460 } 461 462 // Add the search filter. 463 urlBuffer.append(QUESTION_CHAR); 464 if (filter != null) { 465 this.filter = filter; 466 urlBuffer.append(this.filter); 467 } else { 468 this.filter = DEFAULT_FILTER; 469 } 470 471 urlString = urlBuffer.toString(); 472 } 473 474 private LDAPUrl(final String urlString, final Schema schema) { 475 this.urlString = urlString; 476 477 // Parse the url and build the LDAP URL. 478 final int schemeIdx = urlString.indexOf("://"); 479 if (schemeIdx < 0) { 480 throw new LocalizedIllegalArgumentException(ERR_LDAPURL_NO_SCHEME.get(urlString)); 481 } 482 483 final String scheme = StaticUtils.toLowerCase(urlString.substring(0, schemeIdx)); 484 if (DEFAULT_URL_SCHEME.equalsIgnoreCase(scheme)) { 485 // Default ldap scheme. 486 isSecured = false; 487 } else if (SSL_URL_SCHEME.equalsIgnoreCase(scheme)) { 488 isSecured = true; 489 } else { 490 throw new LocalizedIllegalArgumentException(ERR_LDAPURL_BAD_SCHEME.get(urlString, scheme)); 491 } 492 493 final int urlLength = urlString.length(); 494 final int hostPortIdx = urlString.indexOf(SLASH_CHAR, schemeIdx + 3); 495 final StringBuilder builder = new StringBuilder(); 496 if (hostPortIdx < 0) { 497 // We got anything here like the host and port? 498 if (urlLength > schemeIdx + 3) { 499 final String hostAndPort = urlString.substring(schemeIdx + 3, urlLength); 500 port = parseHostPort(urlString, hostAndPort, builder); 501 host = builder.toString(); 502 builder.setLength(0); 503 } else { 504 // Nothing else is specified apart from the scheme. 505 // Use the default settings and return from here. 506 host = DEFAULT_HOST; 507 port = isSecured ? DEFAULT_SSL_PORT : DEFAULT_PORT; 508 } 509 name = DEFAULT_DN; 510 scope = DEFAULT_SCOPE; 511 filter = DEFAULT_FILTER; 512 attributes = Collections.emptyList(); 513 return; 514 } 515 516 final String hostAndPort = urlString.substring(schemeIdx + 3, hostPortIdx); 517 // assign the host and port. 518 port = parseHostPort(urlString, hostAndPort, builder); 519 host = builder.toString(); 520 builder.setLength(0); 521 522 // Parse the dn. 523 DN parsedDN = null; 524 final int dnIdx = urlString.indexOf(QUESTION_CHAR, hostPortIdx + 1); 525 526 if (dnIdx < 0) { 527 // Whatever we have here is the dn. 528 final String dnStr = urlString.substring(hostPortIdx + 1, urlLength); 529 percentDecoder(urlString, hostPortIdx + 1, dnStr, builder); 530 try { 531 parsedDN = DN.valueOf(builder.toString(), schema); 532 } catch (final LocalizedIllegalArgumentException e) { 533 final LocalizableMessage msg = 534 ERR_LDAPURL_INVALID_DN.get(urlString, e.getMessageObject()); 535 throw new LocalizedIllegalArgumentException(msg); 536 } 537 builder.setLength(0); 538 name = parsedDN; 539 scope = DEFAULT_SCOPE; 540 filter = DEFAULT_FILTER; 541 attributes = Collections.emptyList(); 542 return; 543 } 544 545 final String dnStr = urlString.substring(hostPortIdx + 1, dnIdx); 546 if (dnStr.length() == 0) { 547 parsedDN = DEFAULT_DN; 548 } else { 549 percentDecoder(urlString, hostPortIdx + 1, dnStr, builder); 550 try { 551 parsedDN = DN.valueOf(builder.toString(), schema); 552 } catch (final LocalizedIllegalArgumentException e) { 553 final LocalizableMessage msg = 554 ERR_LDAPURL_INVALID_DN.get(urlString, e.getMessageObject()); 555 throw new LocalizedIllegalArgumentException(msg); 556 } 557 builder.setLength(0); 558 } 559 name = parsedDN; 560 561 // Find out the attributes. 562 final int attrIdx = urlString.indexOf(QUESTION_CHAR, dnIdx + 1); 563 if (attrIdx < 0) { 564 attributes = Collections.emptyList(); 565 scope = DEFAULT_SCOPE; 566 filter = DEFAULT_FILTER; 567 return; 568 } 569 attributes = parseAttributes(urlString.substring(dnIdx + 1, attrIdx)); 570 571 // Find the scope. 572 final int scopeIdx = urlString.indexOf(QUESTION_CHAR, attrIdx + 1); 573 if (scopeIdx < 0) { 574 scope = DEFAULT_SCOPE; 575 filter = DEFAULT_FILTER; 576 return; 577 } 578 scope = parseScope(urlString.substring(attrIdx + 1, scopeIdx)); 579 580 // Last one is filter. 581 final String parsedFilter = urlString.substring(scopeIdx + 1, urlLength); 582 if (parsedFilter.length() > 0) { 583 // Clear what we already have. 584 builder.setLength(0); 585 percentDecoder(urlString, scopeIdx + 1, parsedFilter, builder); 586 try { 587 this.filter = Filter.valueOf(builder.toString()); 588 } catch (final LocalizedIllegalArgumentException e) { 589 final LocalizableMessage msg = 590 ERR_LDAPURL_INVALID_FILTER.get(urlString, e.getMessageObject()); 591 throw new LocalizedIllegalArgumentException(msg); 592 } 593 } else { 594 this.filter = DEFAULT_FILTER; 595 } 596 } 597 598 private List<String> parseAttributes(final String attrDesc) { 599 final StringTokenizer token = new StringTokenizer(attrDesc, String.valueOf(COMMA_CHAR)); 600 final List<String> parsedAttrs = new ArrayList<>(token.countTokens()); 601 while (token.hasMoreElements()) { 602 parsedAttrs.add(token.nextToken()); 603 } 604 return Collections.unmodifiableList(parsedAttrs); 605 } 606 607 private SearchScope parseScope(String scopeDef) { 608 final String scope = toLowerCase(scopeDef); 609 for (final SearchScope sscope : SearchScope.values()) { 610 if (sscope.toString().equals(scope)) { 611 return sscope; 612 } 613 } 614 return SearchScope.BASE_OBJECT; 615 } 616 617 /** 618 * Creates a new search request containing the parameters of this LDAP URL. 619 * 620 * @return A new search request containing the parameters of this LDAP URL. 621 */ 622 public SearchRequest asSearchRequest() { 623 final SearchRequest request = Requests.newSearchRequest(name, scope, filter); 624 for (final String a : attributes) { 625 request.addAttribute(a); 626 } 627 return request; 628 } 629 630 /** {@inheritDoc} */ 631 @Override 632 public boolean equals(final Object o) { 633 if (o == this) { 634 return true; 635 } else if (o instanceof LDAPUrl) { 636 final String s1 = toNormalizedString(); 637 final String s2 = ((LDAPUrl) o).toNormalizedString(); 638 return s1.equals(s2); 639 } else { 640 return false; 641 } 642 } 643 644 /** 645 * Returns an unmodifiable list containing the attributes to be included 646 * with each entry that matches the search criteria. Attributes that are 647 * sub-types of listed attributes are implicitly included. If the returned 648 * list is empty then all user attributes will be included by default. 649 * 650 * @return An unmodifiable list containing the attributes to be included 651 * with each entry that matches the search criteria. 652 */ 653 public List<String> getAttributes() { 654 return attributes; 655 } 656 657 /** 658 * Returns the search filter associated with this LDAP URL. 659 * 660 * @return The search filter associated with this LDAP URL. 661 */ 662 public Filter getFilter() { 663 return filter; 664 } 665 666 /** 667 * Returns the name or IP address in dotted format of the LDAP server 668 * referenced by this LDAP URL. For example, {@code ldap.server1.com} or 669 * {@code 192.202.185.90}. Use {@code null} for the local host. 670 * 671 * @return A name or IP address in dotted format of the LDAP server 672 * referenced by this LDAP URL. 673 */ 674 public String getHost() { 675 return host; 676 } 677 678 /** 679 * Returns the distinguished name of the base entry relative to which the 680 * search is to be performed. 681 * 682 * @return The distinguished name of the base entry relative to which the 683 * search is to be performed. 684 */ 685 public DN getName() { 686 return name; 687 } 688 689 /** 690 * Returns the port number of the LDAP server referenced by this LDAP URL. 691 * 692 * @return The port number of the LDAP server referenced by this LDAP URL. 693 */ 694 public int getPort() { 695 return port; 696 } 697 698 /** 699 * Returns the search scope associated with this LDAP URL. 700 * 701 * @return The search scope associated with this LDAP URL. 702 */ 703 public SearchScope getScope() { 704 return scope; 705 } 706 707 /** {@inheritDoc} */ 708 @Override 709 public int hashCode() { 710 final String s = toNormalizedString(); 711 return s.hashCode(); 712 } 713 714 /** 715 * Returns {@code true} if this LDAP URL should use LDAPS or {@code false} 716 * if it should use LDAP. 717 * 718 * @return {@code true} if this LDAP URL should use LDAPS or {@code false} 719 * if it should use LDAP. 720 */ 721 public boolean isSecure() { 722 return isSecured; 723 } 724 725 /** {@inheritDoc} */ 726 @Override 727 public String toString() { 728 return urlString; 729 } 730 731 private int parseHostPort(final String urlString, final String hostAndPort, 732 final StringBuilder host) { 733 Reject.ifNull(urlString); 734 Reject.ifNull(hostAndPort); 735 Reject.ifNull(host); 736 int urlPort = isSecured ? DEFAULT_SSL_PORT : DEFAULT_PORT; 737 if (hostAndPort.length() == 0) { 738 host.append(DEFAULT_HOST); 739 return urlPort; 740 } 741 final int colonIdx = hostAndPort.indexOf(':'); 742 if (colonIdx < 0) { 743 // port is not specified. 744 host.append(hostAndPort); 745 return urlPort; 746 } 747 748 String s = hostAndPort.substring(0, colonIdx); 749 if (s.length() == 0) { 750 // Use the default host as we allow only the port to be 751 // specified. 752 host.append(DEFAULT_HOST); 753 } else { 754 host.append(s); 755 } 756 s = hostAndPort.substring(colonIdx + 1, hostAndPort.length()); 757 try { 758 urlPort = Integer.parseInt(s); 759 } catch (final NumberFormatException e) { 760 throw new LocalizedIllegalArgumentException(ERR_LDAPURL_CANNOT_DECODE_PORT.get(urlString, s)); 761 } 762 763 // Check the validity of the port. 764 if (urlPort < 1 || urlPort > 65535) { 765 throw new LocalizedIllegalArgumentException(ERR_LDAPURL_INVALID_PORT.get(urlString, urlPort)); 766 } 767 return urlPort; 768 } 769 770 private String toNormalizedString() { 771 if (normalizedURL == null) { 772 final StringBuilder builder = new StringBuilder(); 773 if (this.isSecured) { 774 builder.append(SSL_URL_SCHEME); 775 } else { 776 builder.append(DEFAULT_URL_SCHEME); 777 } 778 builder.append("://"); 779 builder.append(host); 780 builder.append(COLON_CHAR); 781 builder.append(port); 782 builder.append(SLASH_CHAR); 783 percentEncoder(name.toString(), builder); 784 builder.append(QUESTION_CHAR); 785 final int sz = attributes.size(); 786 for (int i = 0; i < sz; i++) { 787 if (i > 0) { 788 builder.append(COMMA_CHAR); 789 } 790 builder.append(attributes.get(i)); 791 } 792 builder.append(QUESTION_CHAR); 793 builder.append(scope); 794 builder.append(QUESTION_CHAR); 795 percentEncoder(filter.toString(), builder); 796 normalizedURL = builder.toString(); 797 } 798 return normalizedURL; 799 } 800}