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