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}