001/*
002 * Copyright 2007-2014 UnboundID Corp.
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2008-2014 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 */
021package com.unboundid.ldap.sdk;
022
023
024
025import java.io.Serializable;
026import java.nio.ByteBuffer;
027import java.util.ArrayList;
028
029import com.unboundid.util.NotMutable;
030import com.unboundid.util.ThreadSafety;
031import com.unboundid.util.ThreadSafetyLevel;
032
033import static com.unboundid.ldap.sdk.LDAPMessages.*;
034import static com.unboundid.util.Debug.*;
035import static com.unboundid.util.StaticUtils.*;
036import 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)
119public 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}