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