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