001/*
002 * Copyright 2007-2018 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2008-2018 Ping Identity Corporation
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 (final 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 (final 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          final 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              byteBuffer.put(b);
1003              break;
1004            case '1':
1005              byteBuffer.put((byte) (b | 0x01));
1006              break;
1007            case '2':
1008              byteBuffer.put((byte) (b | 0x02));
1009              break;
1010            case '3':
1011              byteBuffer.put((byte) (b | 0x03));
1012              break;
1013            case '4':
1014              byteBuffer.put((byte) (b | 0x04));
1015              break;
1016            case '5':
1017              byteBuffer.put((byte) (b | 0x05));
1018              break;
1019            case '6':
1020              byteBuffer.put((byte) (b | 0x06));
1021              break;
1022            case '7':
1023              byteBuffer.put((byte) (b | 0x07));
1024              break;
1025            case '8':
1026              byteBuffer.put((byte) (b | 0x08));
1027              break;
1028            case '9':
1029              byteBuffer.put((byte) (b | 0x09));
1030              break;
1031            case 'a':
1032            case 'A':
1033              byteBuffer.put((byte) (b | 0x0A));
1034              break;
1035            case 'b':
1036            case 'B':
1037              byteBuffer.put((byte) (b | 0x0B));
1038              break;
1039            case 'c':
1040            case 'C':
1041              byteBuffer.put((byte) (b | 0x0C));
1042              break;
1043            case 'd':
1044            case 'D':
1045              byteBuffer.put((byte) (b | 0x0D));
1046              break;
1047            case 'e':
1048            case 'E':
1049              byteBuffer.put((byte) (b | 0x0E));
1050              break;
1051            case 'f':
1052            case 'F':
1053              byteBuffer.put((byte) (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          if ((pos < length) && (s.charAt(pos) != '%'))
1062          {
1063            break;
1064          }
1065        }
1066
1067        byteBuffer.flip();
1068        final byte[] byteArray = new byte[byteBuffer.limit()];
1069        byteBuffer.get(byteArray);
1070
1071        buffer.append(toUTF8String(byteArray));
1072      }
1073      else
1074      {
1075        buffer.append(c);
1076      }
1077    }
1078
1079    return buffer.toString();
1080  }
1081
1082
1083
1084  /**
1085   * Appends an encoded version of the provided string to the given buffer.  Any
1086   * special characters contained in the string will be replaced with byte
1087   * representations consisting of one percent sign and two hexadecimal digits
1088   * for each byte in the special character.
1089   *
1090   * @param  s       The string to be encoded.
1091   * @param  buffer  The buffer to which the encoded string will be written.
1092   */
1093  private static void percentEncode(final String s, final StringBuilder buffer)
1094  {
1095    final int length = s.length();
1096    for (int i=0; i < length; i++)
1097    {
1098      final char c = s.charAt(i);
1099
1100      switch (c)
1101      {
1102        case 'A':
1103        case 'B':
1104        case 'C':
1105        case 'D':
1106        case 'E':
1107        case 'F':
1108        case 'G':
1109        case 'H':
1110        case 'I':
1111        case 'J':
1112        case 'K':
1113        case 'L':
1114        case 'M':
1115        case 'N':
1116        case 'O':
1117        case 'P':
1118        case 'Q':
1119        case 'R':
1120        case 'S':
1121        case 'T':
1122        case 'U':
1123        case 'V':
1124        case 'W':
1125        case 'X':
1126        case 'Y':
1127        case 'Z':
1128        case 'a':
1129        case 'b':
1130        case 'c':
1131        case 'd':
1132        case 'e':
1133        case 'f':
1134        case 'g':
1135        case 'h':
1136        case 'i':
1137        case 'j':
1138        case 'k':
1139        case 'l':
1140        case 'm':
1141        case 'n':
1142        case 'o':
1143        case 'p':
1144        case 'q':
1145        case 'r':
1146        case 's':
1147        case 't':
1148        case 'u':
1149        case 'v':
1150        case 'w':
1151        case 'x':
1152        case 'y':
1153        case 'z':
1154        case '0':
1155        case '1':
1156        case '2':
1157        case '3':
1158        case '4':
1159        case '5':
1160        case '6':
1161        case '7':
1162        case '8':
1163        case '9':
1164        case '-':
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          buffer.append(c);
1180          break;
1181
1182        default:
1183          final byte[] charBytes = getBytes(new String(new char[] { c }));
1184          for (final byte b : charBytes)
1185          {
1186            buffer.append('%');
1187            toHex(b, buffer);
1188          }
1189          break;
1190      }
1191    }
1192  }
1193
1194
1195
1196  /**
1197   * Retrieves the scheme for this LDAP URL.  It will either be "ldap", "ldaps",
1198   * or "ldapi".
1199   *
1200   * @return  The scheme for this LDAP URL.
1201   */
1202  public String getScheme()
1203  {
1204    return scheme;
1205  }
1206
1207
1208
1209  /**
1210   * Retrieves the host for this LDAP URL.
1211   *
1212   * @return  The host for this LDAP URL, or {@code null} if the URL does not
1213   *          include a host and the client is supposed to have some external
1214   *          knowledge of what the host should be.
1215   */
1216  public String getHost()
1217  {
1218    return host;
1219  }
1220
1221
1222
1223  /**
1224   * Indicates whether the URL explicitly included a host address.
1225   *
1226   * @return  {@code true} if the URL explicitly included a host address, or
1227   *          {@code false} if it did not.
1228   */
1229  public boolean hostProvided()
1230  {
1231    return (host != null);
1232  }
1233
1234
1235
1236  /**
1237   * Retrieves the port for this LDAP URL.
1238   *
1239   * @return  The port for this LDAP URL.
1240   */
1241  public int getPort()
1242  {
1243    return port;
1244  }
1245
1246
1247
1248  /**
1249   * Indicates whether the URL explicitly included a port number.
1250   *
1251   * @return  {@code true} if the URL explicitly included a port number, or
1252   *          {@code false} if it did not and the default should be used.
1253   */
1254  public boolean portProvided()
1255  {
1256    return portProvided;
1257  }
1258
1259
1260
1261  /**
1262   * Retrieves the base DN for this LDAP URL.
1263   *
1264   * @return  The base DN for this LDAP URL.
1265   */
1266  public DN getBaseDN()
1267  {
1268    return baseDN;
1269  }
1270
1271
1272
1273  /**
1274   * Indicates whether the URL explicitly included a base DN.
1275   *
1276   * @return  {@code true} if the URL explicitly included a base DN, or
1277   *          {@code false} if it did not and the default should be used.
1278   */
1279  public boolean baseDNProvided()
1280  {
1281    return baseDNProvided;
1282  }
1283
1284
1285
1286  /**
1287   * Retrieves the attribute list for this LDAP URL.
1288   *
1289   * @return  The attribute list for this LDAP URL.
1290   */
1291  public String[] getAttributes()
1292  {
1293    return attributes;
1294  }
1295
1296
1297
1298  /**
1299   * Indicates whether the URL explicitly included an attribute list.
1300   *
1301   * @return  {@code true} if the URL explicitly included an attribute list, or
1302   *          {@code false} if it did not and the default should be used.
1303   */
1304  public boolean attributesProvided()
1305  {
1306    return attributesProvided;
1307  }
1308
1309
1310
1311  /**
1312   * Retrieves the scope for this LDAP URL.
1313   *
1314   * @return  The scope for this LDAP URL.
1315   */
1316  public SearchScope getScope()
1317  {
1318    return scope;
1319  }
1320
1321
1322
1323  /**
1324   * Indicates whether the URL explicitly included a search scope.
1325   *
1326   * @return  {@code true} if the URL explicitly included a search scope, or
1327   *          {@code false} if it did not and the default should be used.
1328   */
1329  public boolean scopeProvided()
1330  {
1331    return scopeProvided;
1332  }
1333
1334
1335
1336  /**
1337   * Retrieves the filter for this LDAP URL.
1338   *
1339   * @return  The filter for this LDAP URL.
1340   */
1341  public Filter getFilter()
1342  {
1343    return filter;
1344  }
1345
1346
1347
1348  /**
1349   * Indicates whether the URL explicitly included a search filter.
1350   *
1351   * @return  {@code true} if the URL explicitly included a search filter, or
1352   *          {@code false} if it did not and the default should be used.
1353   */
1354  public boolean filterProvided()
1355  {
1356    return filterProvided;
1357  }
1358
1359
1360
1361  /**
1362   * Creates a search request containing the base DN, scope, filter, and
1363   * requested attributes from this LDAP URL.
1364   *
1365   * @return  The search request created from the base DN, scope, filter, and
1366   *          requested attributes from this LDAP URL.
1367   */
1368  public SearchRequest toSearchRequest()
1369  {
1370    return new SearchRequest(baseDN.toString(), scope, filter, attributes);
1371  }
1372
1373
1374
1375  /**
1376   * Retrieves a hash code for this LDAP URL.
1377   *
1378   * @return  A hash code for this LDAP URL.
1379   */
1380  @Override()
1381  public int hashCode()
1382  {
1383    return toNormalizedString().hashCode();
1384  }
1385
1386
1387
1388  /**
1389   * Indicates whether the provided object is equal to this LDAP URL.  In order
1390   * to be considered equal, the provided object must be an LDAP URL with the
1391   * same normalized string representation.
1392   *
1393   * @param  o  The object for which to make the determination.
1394   *
1395   * @return  {@code true} if the provided object is equal to this LDAP URL, or
1396   *          {@code false} if not.
1397   */
1398  @Override()
1399  public boolean equals(final Object o)
1400  {
1401    if (o == null)
1402    {
1403      return false;
1404    }
1405
1406    if (o == this)
1407    {
1408      return true;
1409    }
1410
1411    if (! (o instanceof LDAPURL))
1412    {
1413      return false;
1414    }
1415
1416    final LDAPURL url = (LDAPURL) o;
1417    return toNormalizedString().equals(url.toNormalizedString());
1418  }
1419
1420
1421
1422  /**
1423   * Retrieves a string representation of this LDAP URL.
1424   *
1425   * @return  A string representation of this LDAP URL.
1426   */
1427  @Override()
1428  public String toString()
1429  {
1430    return urlString;
1431  }
1432
1433
1434
1435  /**
1436   * Retrieves a normalized string representation of this LDAP URL.
1437   *
1438   * @return  A normalized string representation of this LDAP URL.
1439   */
1440  public String toNormalizedString()
1441  {
1442    if (normalizedURLString == null)
1443    {
1444      final StringBuilder buffer = new StringBuilder();
1445      toNormalizedString(buffer);
1446      normalizedURLString = buffer.toString();
1447    }
1448
1449    return normalizedURLString;
1450  }
1451
1452
1453
1454  /**
1455   * Appends a normalized string representation of this LDAP URL to the provided
1456   * buffer.
1457   *
1458   * @param  buffer  The buffer to which to append the normalized string
1459   *                 representation of this LDAP URL.
1460   */
1461  public void toNormalizedString(final StringBuilder buffer)
1462  {
1463    buffer.append(scheme);
1464    buffer.append("://");
1465
1466    if (host != null)
1467    {
1468      if (host.indexOf(':') >= 0)
1469      {
1470        buffer.append('[');
1471        buffer.append(toLowerCase(host));
1472        buffer.append(']');
1473      }
1474      else
1475      {
1476        buffer.append(toLowerCase(host));
1477      }
1478    }
1479
1480    if (! scheme.equals("ldapi"))
1481    {
1482      buffer.append(':');
1483      buffer.append(port);
1484    }
1485
1486    buffer.append('/');
1487    percentEncode(baseDN.toNormalizedString(), buffer);
1488    buffer.append('?');
1489
1490    for (int i=0; i < attributes.length; i++)
1491    {
1492      if (i > 0)
1493      {
1494        buffer.append(',');
1495      }
1496
1497      buffer.append(toLowerCase(attributes[i]));
1498    }
1499
1500    buffer.append('?');
1501    switch (scope.intValue())
1502    {
1503      case 0:  // BASE
1504        buffer.append("base");
1505        break;
1506      case 1:  // ONE
1507        buffer.append("one");
1508        break;
1509      case 2:  // SUB
1510        buffer.append("sub");
1511        break;
1512      case 3:  // SUBORDINATE_SUBTREE
1513        buffer.append("subordinates");
1514        break;
1515    }
1516
1517    buffer.append('?');
1518    percentEncode(filter.toNormalizedString(), buffer);
1519  }
1520}