001    /*
002     * Copyright 2007-2015 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2008-2015 UnboundID Corp.
007     *
008     * This program is free software; you can redistribute it and/or modify
009     * it under the terms of the GNU General Public License (GPLv2 only)
010     * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011     * as published by the Free Software Foundation.
012     *
013     * This program is distributed in the hope that it will be useful,
014     * but WITHOUT ANY WARRANTY; without even the implied warranty of
015     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016     * GNU General Public License for more details.
017     *
018     * You should have received a copy of the GNU General Public License
019     * along with this program; if not, see <http://www.gnu.org/licenses>.
020     */
021    package com.unboundid.ldap.sdk;
022    
023    
024    
025    import java.io.Serializable;
026    import java.nio.ByteBuffer;
027    import java.util.ArrayList;
028    
029    
030    import static com.unboundid.ldap.sdk.LDAPMessages.*;
031    import static com.unboundid.util.Debug.*;
032    import static com.unboundid.util.StaticUtils.*;
033    import static com.unboundid.util.Validator.*;
034    
035    
036    
037    /**
038     * This class provides a data structure for interacting with LDAP URLs.  It may
039     * be used to encode and decode URLs, as well as access the various elements
040     * that they contain.  Note that this implementation currently does not support
041     * the use of extensions in an LDAP URL.
042     * <BR><BR>
043     * The components that may be included in an LDAP URL include:
044     * <UL>
045     *   <LI>Scheme -- This specifies the protocol to use when communicating with
046     *       the server.  The official LDAP URL specification only allows a scheme
047     *       of "{@code ldap}", but this implementation also supports the use of the
048     *       "{@code ldaps}" scheme to indicate that clients should attempt to
049     *       perform SSL-based communication with the target server (LDAPS) rather
050     *       than unencrypted LDAP.  It will also accept "{@code ldapi}", which is
051     *       LDAP over UNIX domain sockets, although the LDAP SDK does not directly
052     *       support that mechanism of communication.</LI>
053     *   <LI>Host -- This specifies the address of the directory server to which the
054     *       URL refers.  If no host is provided, then it is expected that the
055     *       client has some prior knowledge of the host (it often implies the same
056     *       server from which the URL was retrieved).</LI>
057     *   <LI>Port -- This specifies the port of the directory server to which the
058     *       URL refers.  If no host or port is provided, then it is assumed that
059     *       the client has some prior knowledge of the instance to use (it often
060     *       implies the same instance from which the URL was retrieved).  If a host
061     *       is provided without a port, then it should be assumed that the standard
062     *       LDAP port of 389 should be used (or the standard LDAPS port of 636 if
063     *       the scheme is "{@code ldaps}", or a value of 0 if the scheme is
064     *       "{@code ldapi}").</LI>
065     *   <LI>Base DN -- This specifies the base DN for the URL.  If no base DN is
066     *       provided, then a default of the null DN should be assumed.</LI>
067     *   <LI>Requested attributes -- This specifies the set of requested attributes
068     *       for the URL.  If no attributes are specified, then the behavior should
069     *       be the same as if no attributes had been provided for a search request
070     *       (i.e., all user attributes should be included).
071     *       <BR><BR>
072     *       In the string representation of an LDAP URL, the names of the requested
073     *       attributes (if more than one is provided) should be separated by
074     *       commas.</LI>
075     *   <LI>Scope -- This specifies the scope for the URL.  It should be one of the
076     *       standard scope values as defined in the {@code SearchRequest}
077     *       class.  If no scope is provided, then it should be assumed that a
078     *       scope of {@code SearchScope#BASE} should be used.
079     *       <BR><BR>
080     *       In the string representation, the names of the scope values that are
081     *       allowed include:
082     *       <UL>
083     *         <LI>base -- Equivalent to {@code SearchScope#BASE}.</LI>
084     *         <LI>one -- Equivalent to {@code SearchScope#ONE}.</LI>
085     *         <LI>sub -- Equivalent to {@code SearchScope#SUB}.</LI>
086     *         <LI>subordinates -- Equivalent to
087     *             {@code SearchScope#SUBORDINATE_SUBTREE}.</LI>
088     *       </UL></LI>
089     *   <LI>Filter -- This specifies the filter for the URL.  If no filter is
090     *       provided, then a default of "{@code (objectClass=*)}" should be
091     *       assumed.</LI>
092     * </UL>
093     * An LDAP URL encapsulates many of the properties of a search request, and in
094     * fact the {@code LDAPURL#toSearchRequest} method may be used  to create a
095     * {@code SearchRequest} object from an LDAP URL.
096     * <BR><BR>
097     * See <A HREF="http://www.ietf.org/rfc/rfc4516.txt">RFC 4516</A> for a complete
098     * description of the LDAP URL syntax.  Some examples of LDAP URLs include:
099     * <UL>
100     *   <LI>{@code ldap://} -- This is the smallest possible LDAP URL that can be
101     *       represented.  The default values will be used for all components other
102     *       than the scheme.</LI>
103     *   <LI>{@code
104     *        ldap://server.example.com:1234/dc=example,dc=com?cn,sn?sub?(uid=john)}
105     *       -- This is an example of a URL containing all of the elements.  The
106     *       scheme is "{@code ldap}", the host is "{@code server.example.com}",
107     *       the port is "{@code 1234}", the base DN is "{@code dc=example,dc=com}",
108     *       the requested attributes are "{@code cn}" and "{@code sn}", the scope
109     *       is "{@code sub}" (which indicates a subtree scope equivalent to
110     *       {@code SearchScope#SUB}), and a filter of
111     *       "{@code (uid=john)}".</LI>
112     * </UL>
113     */
114    public final class LDAPURL
115           implements Serializable
116    {
117      /**
118       * The default filter that will be used if none is provided.
119       */
120      private static final Filter DEFAULT_FILTER =
121           Filter.createPresenceFilter("objectClass");
122    
123    
124    
125      /**
126       * The default port number that will be used for LDAP URLs if none is
127       * provided.
128       */
129      public static final int DEFAULT_LDAP_PORT = 389;
130    
131    
132    
133      /**
134       * The default port number that will be used for LDAPS URLs if none is
135       * provided.
136       */
137      public static final int DEFAULT_LDAPS_PORT = 636;
138    
139    
140    
141      /**
142       * The default port number that will be used for LDAPI URLs if none is
143       * provided.
144       */
145      public static final int DEFAULT_LDAPI_PORT = 0;
146    
147    
148    
149      /**
150       * The default scope that will be used if none is provided.
151       */
152      private static final SearchScope DEFAULT_SCOPE = SearchScope.BASE;
153    
154    
155    
156      /**
157       * The default base DN that will be used if none is provided.
158       */
159      private static final DN DEFAULT_BASE_DN = DN.NULL_DN;
160    
161    
162    
163      /**
164       * The default set of attributes that will be used if none is provided.
165       */
166      private static final String[] DEFAULT_ATTRIBUTES = NO_STRINGS;
167    
168    
169    
170      /**
171       * The serial version UID for this serializable class.
172       */
173      private static final long serialVersionUID = 3420786933570240493L;
174    
175    
176    
177      // Indicates whether the attribute list was provided in the URL.
178      private final boolean attributesProvided;
179    
180      // Indicates whether the base DN was provided in the URL.
181      private final boolean baseDNProvided;
182    
183      // Indicates whether the filter was provided in the URL.
184      private final boolean filterProvided;
185    
186      // Indicates whether the port was provided in the URL.
187      private final boolean portProvided;
188    
189      // Indicates whether the scope was provided in the URL.
190      private final boolean scopeProvided;
191    
192      // The base DN used by this URL.
193      private final DN baseDN;
194    
195      // The filter used by this URL.
196      private final Filter filter;
197    
198      // The port used by this URL.
199      private final int port;
200    
201      // The search scope used by this URL.
202      private final SearchScope scope;
203    
204      // The host used by this URL.
205      private final String host;
206    
207      // The normalized representation of this LDAP URL.
208      private volatile String normalizedURLString;
209    
210      // The scheme used by this LDAP URL.  The standard only accepts "ldap", but
211      // we will also accept "ldaps" and "ldapi".
212      private final String scheme;
213    
214      // The string representation of this LDAP URL.
215      private final String urlString;
216    
217      // The set of attributes included in this URL.
218      private final String[] attributes;
219    
220    
221    
222      /**
223       * Creates a new LDAP URL from the provided string representation.
224       *
225       * @param  urlString  The string representation for this LDAP URL.  It must
226       *                    not be {@code null}.
227       *
228       * @throws  LDAPException  If the provided URL string cannot be parsed as an
229       *                         LDAP URL.
230       */
231      public LDAPURL(final String urlString)
232             throws LDAPException
233      {
234        ensureNotNull(urlString);
235    
236        this.urlString = urlString;
237    
238    
239        // Find the location of the first colon.  It should mark the end of the
240        // scheme.
241        final int colonPos = urlString.indexOf("://");
242        if (colonPos < 0)
243        {
244          throw new LDAPException(ResultCode.DECODING_ERROR,
245                                  ERR_LDAPURL_NO_COLON_SLASHES.get());
246        }
247    
248        scheme = toLowerCase(urlString.substring(0, colonPos));
249        final int defaultPort;
250        if (scheme.equals("ldap"))
251        {
252          defaultPort = DEFAULT_LDAP_PORT;
253        }
254        else if (scheme.equals("ldaps"))
255        {
256          defaultPort = DEFAULT_LDAPS_PORT;
257        }
258        else if (scheme.equals("ldapi"))
259        {
260          defaultPort = DEFAULT_LDAPI_PORT;
261        }
262        else
263        {
264          throw new LDAPException(ResultCode.DECODING_ERROR,
265                                  ERR_LDAPURL_INVALID_SCHEME.get(scheme));
266        }
267    
268    
269        // Look for the first slash after the "://".  It will designate the end of
270        // the hostport section.
271        final int slashPos = urlString.indexOf('/', colonPos+3);
272        if (slashPos < 0)
273        {
274          // This is fine.  It just means that the URL won't have a base DN,
275          // attribute list, scope, or filter, and that the rest of the value is
276          // the hostport element.
277          baseDN             = DEFAULT_BASE_DN;
278          baseDNProvided     = false;
279          attributes         = DEFAULT_ATTRIBUTES;
280          attributesProvided = false;
281          scope              = DEFAULT_SCOPE;
282          scopeProvided      = false;
283          filter             = DEFAULT_FILTER;
284          filterProvided     = false;
285    
286          final String hostPort = urlString.substring(colonPos+3);
287          final StringBuilder hostBuffer = new StringBuilder(hostPort.length());
288          final int portValue = decodeHostPort(hostPort, hostBuffer);
289          if (portValue < 0)
290          {
291            port         = defaultPort;
292            portProvided = false;
293          }
294          else
295          {
296            port         = portValue;
297            portProvided = true;
298          }
299    
300          if (hostBuffer.length() == 0)
301          {
302            host = null;
303          }
304          else
305          {
306            host = hostBuffer.toString();
307          }
308          return;
309        }
310    
311        final String hostPort = urlString.substring(colonPos+3, slashPos);
312        final StringBuilder hostBuffer = new StringBuilder(hostPort.length());
313        final int portValue = decodeHostPort(hostPort, hostBuffer);
314        if (portValue < 0)
315        {
316          port         = defaultPort;
317          portProvided = false;
318        }
319        else
320        {
321          port         = portValue;
322          portProvided = true;
323        }
324    
325        if (hostBuffer.length() == 0)
326        {
327          host = null;
328        }
329        else
330        {
331          host = hostBuffer.toString();
332        }
333    
334    
335        // Look for the first question mark after the slash.  It will designate the
336        // end of the base DN.
337        final int questionMarkPos = urlString.indexOf('?', slashPos+1);
338        if (questionMarkPos < 0)
339        {
340          // This is fine.  It just means that the URL won't have an attribute list,
341          // scope, or filter, and that the rest of the value is the base DN.
342          attributes         = DEFAULT_ATTRIBUTES;
343          attributesProvided = false;
344          scope              = DEFAULT_SCOPE;
345          scopeProvided      = false;
346          filter             = DEFAULT_FILTER;
347          filterProvided     = false;
348    
349          baseDN = new DN(percentDecode(urlString.substring(slashPos+1)));
350          baseDNProvided = (! baseDN.isNullDN());
351          return;
352        }
353    
354        baseDN = new DN(percentDecode(urlString.substring(slashPos+1,
355                                                          questionMarkPos)));
356        baseDNProvided = (! baseDN.isNullDN());
357    
358    
359        // Look for the next question mark.  It will designate the end of the
360        // attribute list.
361        final int questionMark2Pos = urlString.indexOf('?', questionMarkPos+1);
362        if (questionMark2Pos < 0)
363        {
364          // This is fine.  It just means that the URL won't have a scope or filter,
365          // and that the rest of the value is the attribute list.
366          scope          = DEFAULT_SCOPE;
367          scopeProvided  = false;
368          filter         = DEFAULT_FILTER;
369          filterProvided = false;
370    
371          attributes = decodeAttributes(urlString.substring(questionMarkPos+1));
372          attributesProvided = (attributes.length > 0);
373          return;
374        }
375    
376        attributes = decodeAttributes(urlString.substring(questionMarkPos+1,
377                                                          questionMark2Pos));
378        attributesProvided = (attributes.length > 0);
379    
380    
381        // Look for the next question mark.  It will designate the end of the scope.
382        final int questionMark3Pos = urlString.indexOf('?', questionMark2Pos+1);
383        if (questionMark3Pos < 0)
384        {
385          // This is fine.  It just means that the URL won't have a filter, and that
386          // the rest of the value is the scope.
387          filter         = DEFAULT_FILTER;
388          filterProvided = false;
389    
390          final String scopeStr =
391               toLowerCase(urlString.substring(questionMark2Pos+1));
392          if (scopeStr.length() == 0)
393          {
394            scope         = SearchScope.BASE;
395            scopeProvided = false;
396          }
397          else if (scopeStr.equals("base"))
398          {
399            scope         = SearchScope.BASE;
400            scopeProvided = true;
401          }
402          else if (scopeStr.equals("one"))
403          {
404            scope         = SearchScope.ONE;
405            scopeProvided = true;
406          }
407          else if (scopeStr.equals("sub"))
408          {
409            scope         = SearchScope.SUB;
410            scopeProvided = true;
411          }
412          else if (scopeStr.equals("subord") || scopeStr.equals("subordinates"))
413          {
414            scope         = SearchScope.SUBORDINATE_SUBTREE;
415            scopeProvided = true;
416          }
417          else
418          {
419            throw new LDAPException(ResultCode.DECODING_ERROR,
420                                    ERR_LDAPURL_INVALID_SCOPE.get(scopeStr));
421          }
422          return;
423        }
424    
425        final String scopeStr =
426             toLowerCase(urlString.substring(questionMark2Pos+1, questionMark3Pos));
427        if (scopeStr.length() == 0)
428        {
429          scope         = SearchScope.BASE;
430          scopeProvided = false;
431        }
432        else if (scopeStr.equals("base"))
433        {
434          scope         = SearchScope.BASE;
435          scopeProvided = true;
436        }
437        else if (scopeStr.equals("one"))
438        {
439          scope         = SearchScope.ONE;
440          scopeProvided = true;
441        }
442        else if (scopeStr.equals("sub"))
443        {
444          scope         = SearchScope.SUB;
445          scopeProvided = true;
446        }
447            else if (scopeStr.equals("subord") || scopeStr.equals("subordinates"))
448        {
449          scope         = SearchScope.SUBORDINATE_SUBTREE;
450          scopeProvided = true;
451        }
452        else
453        {
454          throw new LDAPException(ResultCode.DECODING_ERROR,
455                                  ERR_LDAPURL_INVALID_SCOPE.get(scopeStr));
456        }
457    
458    
459        // The remainder of the value must be the filter.
460        final String filterStr =
461             percentDecode(urlString.substring(questionMark3Pos+1));
462        if (filterStr.length() == 0)
463        {
464          filter = DEFAULT_FILTER;
465          filterProvided = false;
466        }
467        else
468        {
469          filter = Filter.create(filterStr);
470          filterProvided = true;
471        }
472      }
473    
474    
475    
476      /**
477       * Creates a new LDAP URL with the provided information.
478       *
479       * @param  scheme      The scheme for this LDAP URL.  It must not be
480       *                     {@code null} and must be either "ldap", "ldaps", or
481       *                     "ldapi".
482       * @param  host        The host for this LDAP URL.  It may be {@code null} if
483       *                     no host is to be included.
484       * @param  port        The port for this LDAP URL.  It may be {@code null} if
485       *                     no port is to be included.  If it is provided, it must
486       *                     be between 1 and 65535, inclusive.
487       * @param  baseDN      The base DN for this LDAP URL.  It may be {@code null}
488       *                     if no base DN is to be included.
489       * @param  attributes  The set of requested attributes for this LDAP URL.  It
490       *                     may be {@code null} or empty if no attribute list is to
491       *                     be included.
492       * @param  scope       The scope for this LDAP URL.  It may be {@code null} if
493       *                     no scope is to be included.  Otherwise, it must be a
494       *                     value between zero and three, inclusive.
495       * @param  filter      The filter for this LDAP URL.  It may be {@code null}
496       *                     if no filter is to be included.
497       *
498       * @throws  LDAPException  If there is a problem with any of the provided
499       *                         arguments.
500       */
501      public LDAPURL(final String scheme, final String host, final Integer port,
502                     final DN baseDN, final String[] attributes,
503                     final SearchScope scope, final Filter filter)
504             throws LDAPException
505      {
506        ensureNotNull(scheme);
507    
508        final StringBuilder buffer = new StringBuilder();
509    
510        this.scheme = toLowerCase(scheme);
511        final int defaultPort;
512        if (scheme.equals("ldap"))
513        {
514          defaultPort = DEFAULT_LDAP_PORT;
515        }
516        else if (scheme.equals("ldaps"))
517        {
518          defaultPort = DEFAULT_LDAPS_PORT;
519        }
520        else if (scheme.equals("ldapi"))
521        {
522          defaultPort = DEFAULT_LDAPI_PORT;
523        }
524        else
525        {
526          throw new LDAPException(ResultCode.DECODING_ERROR,
527                                  ERR_LDAPURL_INVALID_SCHEME.get(scheme));
528        }
529    
530        buffer.append(scheme);
531        buffer.append("://");
532    
533        if ((host == null) || (host.length() == 0))
534        {
535          this.host = null;
536        }
537        else
538        {
539          this.host = host;
540          buffer.append(host);
541        }
542    
543        if (port == null)
544        {
545          this.port = defaultPort;
546          portProvided = false;
547        }
548        else
549        {
550          this.port = port;
551          portProvided = true;
552          buffer.append(':');
553          buffer.append(port);
554    
555          if ((port < 1) || (port > 65535))
556          {
557            throw new LDAPException(ResultCode.PARAM_ERROR,
558                                    ERR_LDAPURL_INVALID_PORT.get(port));
559          }
560        }
561    
562        buffer.append('/');
563        if (baseDN == null)
564        {
565          this.baseDN = DEFAULT_BASE_DN;
566          baseDNProvided = false;
567        }
568        else
569        {
570          this.baseDN = baseDN;
571          baseDNProvided = true;
572          percentEncode(baseDN.toString(), buffer);
573        }
574    
575        final boolean continueAppending;
576        if (((attributes == null) || (attributes.length == 0)) && (scope == null) &&
577            (filter == null))
578        {
579          continueAppending = false;
580        }
581        else
582        {
583          continueAppending = true;
584        }
585    
586        if (continueAppending)
587        {
588          buffer.append('?');
589        }
590        if ((attributes == null) || (attributes.length == 0))
591        {
592          this.attributes = DEFAULT_ATTRIBUTES;
593          attributesProvided = false;
594        }
595        else
596        {
597          this.attributes = attributes;
598          attributesProvided = true;
599    
600          for (int i=0; i < attributes.length; i++)
601          {
602            if (i > 0)
603            {
604              buffer.append(',');
605            }
606            buffer.append(attributes[i]);
607          }
608        }
609    
610        if (continueAppending)
611        {
612          buffer.append('?');
613        }
614        if (scope == null)
615        {
616          this.scope = DEFAULT_SCOPE;
617          scopeProvided = false;
618        }
619        else
620        {
621          switch (scope.intValue())
622          {
623            case 0:
624              this.scope = scope;
625              scopeProvided = true;
626              buffer.append("base");
627              break;
628            case 1:
629              this.scope = scope;
630              scopeProvided = true;
631              buffer.append("one");
632              break;
633            case 2:
634              this.scope = scope;
635              scopeProvided = true;
636              buffer.append("sub");
637              break;
638            case 3:
639              this.scope = scope;
640              scopeProvided = true;
641              buffer.append("subordinates");
642              break;
643            default:
644              throw new LDAPException(ResultCode.PARAM_ERROR,
645                                      ERR_LDAPURL_INVALID_SCOPE_VALUE.get(scope));
646          }
647        }
648    
649        if (continueAppending)
650        {
651          buffer.append('?');
652        }
653        if (filter == null)
654        {
655          this.filter = DEFAULT_FILTER;
656          filterProvided = false;
657        }
658        else
659        {
660          this.filter = filter;
661          filterProvided = true;
662          percentEncode(filter.toString(), buffer);
663        }
664    
665        urlString = buffer.toString();
666      }
667    
668    
669    
670      /**
671       * Decodes the provided string as a host and optional port number.
672       *
673       * @param  hostPort    The string to be decoded.
674       * @param  hostBuffer  The buffer to which the decoded host address will be
675       *                     appended.
676       *
677       * @return  The port number decoded from the provided string, or -1 if there
678       *          was no port number.
679       *
680       * @throws  LDAPException  If the provided string cannot be decoded as a
681       *                         hostport element.
682       */
683      private static int decodeHostPort(final String hostPort,
684                                        final StringBuilder hostBuffer)
685              throws LDAPException
686      {
687        final int length = hostPort.length();
688        if (length == 0)
689        {
690          // It's an empty string, so we'll just use the defaults.
691          return -1;
692        }
693    
694        if (hostPort.charAt(0) == '[')
695        {
696          // It starts with a square bracket, which means that the address is an
697          // IPv6 literal address.  Find the closing bracket, and the address
698          // will be inside them.
699          final int closingBracketPos = hostPort.indexOf(']');
700          if (closingBracketPos < 0)
701          {
702            throw new LDAPException(ResultCode.DECODING_ERROR,
703                                    ERR_LDAPURL_IPV6_HOST_MISSING_BRACKET.get());
704          }
705    
706          hostBuffer.append(hostPort.substring(1, closingBracketPos).trim());
707          if (hostBuffer.length() == 0)
708          {
709            throw new LDAPException(ResultCode.DECODING_ERROR,
710                                    ERR_LDAPURL_IPV6_HOST_EMPTY.get());
711          }
712    
713          // The closing bracket must either be the end of the hostport element
714          // (in which case we'll use the default port), or it must be followed by
715          // a colon and an integer (which will be the port).
716          if (closingBracketPos == (length - 1))
717          {
718            return -1;
719          }
720          else
721          {
722            if (hostPort.charAt(closingBracketPos+1) != ':')
723            {
724              throw new LDAPException(ResultCode.DECODING_ERROR,
725                                      ERR_LDAPURL_IPV6_HOST_UNEXPECTED_CHAR.get(
726                                           hostPort.charAt(closingBracketPos+1)));
727            }
728            else
729            {
730              try
731              {
732                final int decodedPort =
733                     Integer.parseInt(hostPort.substring(closingBracketPos+2));
734                if ((decodedPort >= 1) && (decodedPort <= 65535))
735                {
736                  return decodedPort;
737                }
738                else
739                {
740                  throw new LDAPException(ResultCode.DECODING_ERROR,
741                                          ERR_LDAPURL_INVALID_PORT.get(
742                                               decodedPort));
743                }
744              }
745              catch (NumberFormatException nfe)
746              {
747                debugException(nfe);
748                throw new LDAPException(ResultCode.DECODING_ERROR,
749                                        ERR_LDAPURL_PORT_NOT_INT.get(hostPort),
750                                        nfe);
751              }
752            }
753          }
754        }
755    
756    
757        // If we've gotten here, then the address is either a resolvable name or an
758        // IPv4 address.  If there is a colon in the string, then it will separate
759        // the address from the port.  Otherwise, the remaining value will be the
760        // address and we'll use the default port.
761        final int colonPos = hostPort.indexOf(':');
762        if (colonPos < 0)
763        {
764          hostBuffer.append(hostPort);
765          return -1;
766        }
767        else
768        {
769          try
770          {
771            final int decodedPort =
772                 Integer.parseInt(hostPort.substring(colonPos+1));
773            if ((decodedPort >= 1) && (decodedPort <= 65535))
774            {
775              hostBuffer.append(hostPort.substring(0, colonPos));
776              return decodedPort;
777            }
778            else
779            {
780              throw new LDAPException(ResultCode.DECODING_ERROR,
781                                      ERR_LDAPURL_INVALID_PORT.get(decodedPort));
782            }
783          }
784          catch (NumberFormatException nfe)
785          {
786            debugException(nfe);
787            throw new LDAPException(ResultCode.DECODING_ERROR,
788                                    ERR_LDAPURL_PORT_NOT_INT.get(hostPort), nfe);
789          }
790        }
791      }
792    
793    
794    
795      /**
796       * Decodes the contents of the provided string as an attribute list.
797       *
798       * @param  s  The string to decode as an attribute list.
799       *
800       * @return  The array of decoded attribute names.
801       *
802       * @throws  LDAPException  If an error occurred while attempting to decode the
803       *                         attribute list.
804       */
805      private static String[] decodeAttributes(final String s)
806              throws LDAPException
807      {
808        final int length = s.length();
809        if (length == 0)
810        {
811          return DEFAULT_ATTRIBUTES;
812        }
813    
814        final ArrayList<String> attrList = new ArrayList<String>();
815        int startPos = 0;
816        while (startPos < length)
817        {
818          final int commaPos = s.indexOf(',', startPos);
819          if (commaPos < 0)
820          {
821            // There are no more commas, so there can only be one attribute left.
822            final String attrName = s.substring(startPos).trim();
823            if (attrName.length() == 0)
824            {
825              // This is only acceptable if the attribute list is empty (there was
826              // probably a space in the attribute list string, which is technically
827              // not allowed, but we'll accept it).  If the attribute list is not
828              // empty, then there were two consecutive commas, which is not
829              // allowed.
830              if (attrList.isEmpty())
831              {
832                return DEFAULT_ATTRIBUTES;
833              }
834              else
835              {
836                throw new LDAPException(ResultCode.DECODING_ERROR,
837                                        ERR_LDAPURL_ATTRLIST_ENDS_WITH_COMMA.get());
838              }
839            }
840            else
841            {
842              attrList.add(attrName);
843              break;
844            }
845          }
846          else
847          {
848            final String attrName = s.substring(startPos, commaPos).trim();
849            if (attrName.length() == 0)
850            {
851              throw new LDAPException(ResultCode.DECODING_ERROR,
852                                      ERR_LDAPURL_ATTRLIST_EMPTY_ATTRIBUTE.get());
853            }
854            else
855            {
856              attrList.add(attrName);
857              startPos = commaPos+1;
858              if (startPos >= length)
859              {
860                throw new LDAPException(ResultCode.DECODING_ERROR,
861                                        ERR_LDAPURL_ATTRLIST_ENDS_WITH_COMMA.get());
862              }
863            }
864          }
865        }
866    
867        final String[] attributes = new String[attrList.size()];
868        attrList.toArray(attributes);
869        return attributes;
870      }
871    
872    
873    
874      /**
875       * Decodes any percent-encoded values that may be contained in the provided
876       * string.
877       *
878       * @param  s  The string to be decoded.
879       *
880       * @return  The percent-decoded form of the provided string.
881       *
882       * @throws  LDAPException  If a problem occurs while attempting to decode the
883       *                         provided string.
884       */
885      public static String percentDecode(final String s)
886              throws LDAPException
887      {
888        // First, see if there are any percent characters at all in the provided
889        // string.  If not, then just return the string as-is.
890        int firstPercentPos = -1;
891        final int length = s.length();
892        for (int i=0; i < length; i++)
893        {
894          if (s.charAt(i) == '%')
895          {
896            firstPercentPos = i;
897            break;
898          }
899        }
900    
901        if (firstPercentPos < 0)
902        {
903          return s;
904        }
905    
906        int pos = firstPercentPos;
907        final StringBuilder buffer = new StringBuilder(2 * length);
908        buffer.append(s.substring(0, firstPercentPos));
909    
910        while (pos < length)
911        {
912          final char c = s.charAt(pos++);
913          if (c == '%')
914          {
915            if (pos >= length)
916            {
917              throw new LDAPException(ResultCode.DECODING_ERROR,
918                                      ERR_LDAPURL_HEX_STRING_TOO_SHORT.get(s));
919            }
920    
921    
922            final ByteBuffer byteBuffer = ByteBuffer.allocate(length - pos);
923            while (pos < length)
924            {
925              byte b;
926              switch (s.charAt(pos++))
927              {
928                case '0':
929                  b = 0x00;
930                  break;
931                case '1':
932                  b = 0x10;
933                  break;
934                case '2':
935                  b = 0x20;
936                  break;
937                case '3':
938                  b = 0x30;
939                  break;
940                case '4':
941                  b = 0x40;
942                  break;
943                case '5':
944                  b = 0x50;
945                  break;
946                case '6':
947                  b = 0x60;
948                  break;
949                case '7':
950                  b = 0x70;
951                  break;
952                case '8':
953                  b = (byte) 0x80;
954                  break;
955                case '9':
956                  b = (byte) 0x90;
957                  break;
958                case 'a':
959                case 'A':
960                  b = (byte) 0xA0;
961                  break;
962                case 'b':
963                case 'B':
964                  b = (byte) 0xB0;
965                  break;
966                case 'c':
967                case 'C':
968                  b = (byte) 0xC0;
969                  break;
970                case 'd':
971                case 'D':
972                  b = (byte) 0xD0;
973                  break;
974                case 'e':
975                case 'E':
976                  b = (byte) 0xE0;
977                  break;
978                case 'f':
979                case 'F':
980                  b = (byte) 0xF0;
981                  break;
982                default:
983                  throw new LDAPException(ResultCode.DECODING_ERROR,
984                                          ERR_LDAPURL_INVALID_HEX_CHAR.get(
985                                               s.charAt(pos-1)));
986              }
987    
988              if (pos >= length)
989              {
990                throw new LDAPException(ResultCode.DECODING_ERROR,
991                                        ERR_LDAPURL_HEX_STRING_TOO_SHORT.get(s));
992              }
993    
994              switch (s.charAt(pos++))
995              {
996                case '0':
997                  b |= 0x00;
998                  break;
999                case '1':
1000                  b |= 0x01;
1001                  break;
1002                case '2':
1003                  b |= 0x02;
1004                  break;
1005                case '3':
1006                  b |= 0x03;
1007                  break;
1008                case '4':
1009                  b |= 0x04;
1010                  break;
1011                case '5':
1012                  b |= 0x05;
1013                  break;
1014                case '6':
1015                  b |= 0x06;
1016                  break;
1017                case '7':
1018                  b |= 0x07;
1019                  break;
1020                case '8':
1021                  b |= 0x08;
1022                  break;
1023                case '9':
1024                  b |= 0x09;
1025                  break;
1026                case 'a':
1027                case 'A':
1028                  b |= 0x0A;
1029                  break;
1030                case 'b':
1031                case 'B':
1032                  b |= 0x0B;
1033                  break;
1034                case 'c':
1035                case 'C':
1036                  b |= 0x0C;
1037                  break;
1038                case 'd':
1039                case 'D':
1040                  b |= 0x0D;
1041                  break;
1042                case 'e':
1043                case 'E':
1044                  b |= 0x0E;
1045                  break;
1046                case 'f':
1047                case 'F':
1048                  b |= 0x0F;
1049                  break;
1050                default:
1051                  throw new LDAPException(ResultCode.DECODING_ERROR,
1052                                          ERR_LDAPURL_INVALID_HEX_CHAR.get(
1053                                               s.charAt(pos-1)));
1054              }
1055    
1056              byteBuffer.put(b);
1057              if ((pos < length) && (s.charAt(pos) != '%'))
1058              {
1059                break;
1060              }
1061            }
1062    
1063            byteBuffer.flip();
1064            final byte[] byteArray = new byte[byteBuffer.limit()];
1065            byteBuffer.get(byteArray);
1066    
1067            buffer.append(toUTF8String(byteArray));
1068          }
1069          else
1070          {
1071            buffer.append(c);
1072          }
1073        }
1074    
1075        return buffer.toString();
1076      }
1077    
1078    
1079    
1080      /**
1081       * Appends an encoded version of the provided string to the given buffer.  Any
1082       * special characters contained in the string will be replaced with byte
1083       * representations consisting of one percent sign and two hexadecimal digits
1084       * for each byte in the special character.
1085       *
1086       * @param  s       The string to be encoded.
1087       * @param  buffer  The buffer to which the encoded string will be written.
1088       */
1089      private static void percentEncode(final String s, final StringBuilder buffer)
1090      {
1091        final int length = s.length();
1092        for (int i=0; i < length; i++)
1093        {
1094          final char c = s.charAt(i);
1095    
1096          switch (c)
1097          {
1098            case 'A':
1099            case 'B':
1100            case 'C':
1101            case 'D':
1102            case 'E':
1103            case 'F':
1104            case 'G':
1105            case 'H':
1106            case 'I':
1107            case 'J':
1108            case 'K':
1109            case 'L':
1110            case 'M':
1111            case 'N':
1112            case 'O':
1113            case 'P':
1114            case 'Q':
1115            case 'R':
1116            case 'S':
1117            case 'T':
1118            case 'U':
1119            case 'V':
1120            case 'W':
1121            case 'X':
1122            case 'Y':
1123            case 'Z':
1124            case 'a':
1125            case 'b':
1126            case 'c':
1127            case 'd':
1128            case 'e':
1129            case 'f':
1130            case 'g':
1131            case 'h':
1132            case 'i':
1133            case 'j':
1134            case 'k':
1135            case 'l':
1136            case 'm':
1137            case 'n':
1138            case 'o':
1139            case 'p':
1140            case 'q':
1141            case 'r':
1142            case 's':
1143            case 't':
1144            case 'u':
1145            case 'v':
1146            case 'w':
1147            case 'x':
1148            case 'y':
1149            case 'z':
1150            case '0':
1151            case '1':
1152            case '2':
1153            case '3':
1154            case '4':
1155            case '5':
1156            case '6':
1157            case '7':
1158            case '8':
1159            case '9':
1160            case '-':
1161            case '.':
1162            case '_':
1163            case '~':
1164            case '!':
1165            case '$':
1166            case '&':
1167            case '\'':
1168            case '(':
1169            case ')':
1170            case '*':
1171            case '+':
1172            case ',':
1173            case ';':
1174            case '=':
1175              buffer.append(c);
1176              break;
1177    
1178            default:
1179              final byte[] charBytes = getBytes(new String(new char[] { c }));
1180              for (final byte b : charBytes)
1181              {
1182                buffer.append('%');
1183                toHex(b, buffer);
1184              }
1185              break;
1186          }
1187        }
1188      }
1189    
1190    
1191    
1192      /**
1193       * Retrieves the scheme for this LDAP URL.  It will either be "ldap", "ldaps",
1194       * or "ldapi".
1195       *
1196       * @return  The scheme for this LDAP URL.
1197       */
1198      public String getScheme()
1199      {
1200        return scheme;
1201      }
1202    
1203    
1204    
1205      /**
1206       * Retrieves the host for this LDAP URL.
1207       *
1208       * @return  The host for this LDAP URL, or {@code null} if the URL does not
1209       *          include a host and the client is supposed to have some external
1210       *          knowledge of what the host should be.
1211       */
1212      public String getHost()
1213      {
1214        return host;
1215      }
1216    
1217    
1218    
1219      /**
1220       * Indicates whether the URL explicitly included a host address.
1221       *
1222       * @return  {@code true} if the URL explicitly included a host address, or
1223       *          {@code false} if it did not.
1224       */
1225      public boolean hostProvided()
1226      {
1227        return (host != null);
1228      }
1229    
1230    
1231    
1232      /**
1233       * Retrieves the port for this LDAP URL.
1234       *
1235       * @return  The port for this LDAP URL.
1236       */
1237      public int getPort()
1238      {
1239        return port;
1240      }
1241    
1242    
1243    
1244      /**
1245       * Indicates whether the URL explicitly included a port number.
1246       *
1247       * @return  {@code true} if the URL explicitly included a port number, or
1248       *          {@code false} if it did not and the default should be used.
1249       */
1250      public boolean portProvided()
1251      {
1252        return portProvided;
1253      }
1254    
1255    
1256    
1257      /**
1258       * Retrieves the base DN for this LDAP URL.
1259       *
1260       * @return  The base DN for this LDAP URL.
1261       */
1262      public DN getBaseDN()
1263      {
1264        return baseDN;
1265      }
1266    
1267    
1268    
1269      /**
1270       * Indicates whether the URL explicitly included a base DN.
1271       *
1272       * @return  {@code true} if the URL explicitly included a base DN, or
1273       *          {@code false} if it did not and the default should be used.
1274       */
1275      public boolean baseDNProvided()
1276      {
1277        return baseDNProvided;
1278      }
1279    
1280    
1281    
1282      /**
1283       * Retrieves the attribute list for this LDAP URL.
1284       *
1285       * @return  The attribute list for this LDAP URL.
1286       */
1287      public String[] getAttributes()
1288      {
1289        return attributes;
1290      }
1291    
1292    
1293    
1294      /**
1295       * Indicates whether the URL explicitly included an attribute list.
1296       *
1297       * @return  {@code true} if the URL explicitly included an attribute list, or
1298       *          {@code false} if it did not and the default should be used.
1299       */
1300      public boolean attributesProvided()
1301      {
1302        return attributesProvided;
1303      }
1304    
1305    
1306    
1307      /**
1308       * Retrieves the scope for this LDAP URL.
1309       *
1310       * @return  The scope for this LDAP URL.
1311       */
1312      public SearchScope getScope()
1313      {
1314        return scope;
1315      }
1316    
1317    
1318    
1319      /**
1320       * Indicates whether the URL explicitly included a search scope.
1321       *
1322       * @return  {@code true} if the URL explicitly included a search scope, or
1323       *          {@code false} if it did not and the default should be used.
1324       */
1325      public boolean scopeProvided()
1326      {
1327        return scopeProvided;
1328      }
1329    
1330    
1331    
1332      /**
1333       * Retrieves the filter for this LDAP URL.
1334       *
1335       * @return  The filter for this LDAP URL.
1336       */
1337      public Filter getFilter()
1338      {
1339        return filter;
1340      }
1341    
1342    
1343    
1344      /**
1345       * Indicates whether the URL explicitly included a search filter.
1346       *
1347       * @return  {@code true} if the URL explicitly included a search filter, or
1348       *          {@code false} if it did not and the default should be used.
1349       */
1350      public boolean filterProvided()
1351      {
1352        return filterProvided;
1353      }
1354    
1355    
1356    
1357      /**
1358       * Creates a search request containing the base DN, scope, filter, and
1359       * requested attributes from this LDAP URL.
1360       *
1361       * @return  The search request created from the base DN, scope, filter, and
1362       *          requested attributes from this LDAP URL.
1363       */
1364      public SearchRequest toSearchRequest()
1365      {
1366        return new SearchRequest(baseDN.toString(), scope, filter, attributes);
1367      }
1368    
1369    
1370    
1371      /**
1372       * Retrieves a hash code for this LDAP URL.
1373       *
1374       * @return  A hash code for this LDAP URL.
1375       */
1376      @Override()
1377      public int hashCode()
1378      {
1379        return toNormalizedString().hashCode();
1380      }
1381    
1382    
1383    
1384      /**
1385       * Indicates whether the provided object is equal to this LDAP URL.  In order
1386       * to be considered equal, the provided object must be an LDAP URL with the
1387       * same normalized string representation.
1388       *
1389       * @param  o  The object for which to make the determination.
1390       *
1391       * @return  {@code true} if the provided object is equal to this LDAP URL, or
1392       *          {@code false} if not.
1393       */
1394      @Override()
1395      public boolean equals(final Object o)
1396      {
1397        if (o == null)
1398        {
1399          return false;
1400        }
1401    
1402        if (o == this)
1403        {
1404          return true;
1405        }
1406    
1407        if (! (o instanceof LDAPURL))
1408        {
1409          return false;
1410        }
1411    
1412        final LDAPURL url = (LDAPURL) o;
1413        return toNormalizedString().equals(url.toNormalizedString());
1414      }
1415    
1416    
1417    
1418      /**
1419       * Retrieves a string representation of this LDAP URL.
1420       *
1421       * @return  A string representation of this LDAP URL.
1422       */
1423      @Override()
1424      public String toString()
1425      {
1426        return urlString;
1427      }
1428    
1429    
1430    
1431      /**
1432       * Retrieves a normalized string representation of this LDAP URL.
1433       *
1434       * @return  A normalized string representation of this LDAP URL.
1435       */
1436      public String toNormalizedString()
1437      {
1438        if (normalizedURLString == null)
1439        {
1440          final StringBuilder buffer = new StringBuilder();
1441          toNormalizedString(buffer);
1442          normalizedURLString = buffer.toString();
1443        }
1444    
1445        return normalizedURLString;
1446      }
1447    
1448    
1449    
1450      /**
1451       * Appends a normalized string representation of this LDAP URL to the provided
1452       * buffer.
1453       *
1454       * @param  buffer  The buffer to which to append the normalized string
1455       *                 representation of this LDAP URL.
1456       */
1457      public void toNormalizedString(final StringBuilder buffer)
1458      {
1459        buffer.append(scheme);
1460        buffer.append("://");
1461    
1462        if (host != null)
1463        {
1464          if (host.indexOf(':') >= 0)
1465          {
1466            buffer.append('[');
1467            buffer.append(toLowerCase(host));
1468            buffer.append(']');
1469          }
1470          else
1471          {
1472            buffer.append(toLowerCase(host));
1473          }
1474        }
1475    
1476        if (! scheme.equals("ldapi"))
1477        {
1478          buffer.append(':');
1479          buffer.append(port);
1480        }
1481    
1482        buffer.append('/');
1483        percentEncode(baseDN.toNormalizedString(), buffer);
1484        buffer.append('?');
1485    
1486        for (int i=0; i < attributes.length; i++)
1487        {
1488          if (i > 0)
1489          {
1490            buffer.append(',');
1491          }
1492    
1493          buffer.append(toLowerCase(attributes[i]));
1494        }
1495    
1496        buffer.append('?');
1497        switch (scope.intValue())
1498        {
1499          case 0:  // BASE
1500            buffer.append("base");
1501            break;
1502          case 1:  // ONE
1503            buffer.append("one");
1504            break;
1505          case 2:  // SUB
1506            buffer.append("sub");
1507            break;
1508          case 3:  // SUBORDINATE_SUBTREE
1509            buffer.append("subordinates");
1510            break;
1511        }
1512    
1513        buffer.append('?');
1514        percentEncode(filter.toNormalizedString(), buffer);
1515      }
1516    }