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