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