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}