001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2006-2008 Sun Microsystems, Inc.
015 * Portions Copyright 2012-2016 ForgeRock AS.
016 */
017package org.opends.server.types;
018
019import java.util.Iterator;
020import java.util.LinkedHashSet;
021import java.util.LinkedList;
022import java.util.List;
023import java.util.Objects;
024import java.util.Set;
025import java.util.StringTokenizer;
026
027import org.forgerock.i18n.LocalizableMessage;
028import org.forgerock.i18n.slf4j.LocalizedLogger;
029import org.forgerock.opendj.ldap.DN;
030import org.forgerock.opendj.ldap.ResultCode;
031import org.forgerock.opendj.ldap.SearchScope;
032import org.forgerock.opendj.ldap.schema.CoreSchema;
033
034import static org.forgerock.opendj.ldap.ResultCode.*;
035import static org.opends.messages.UtilityMessages.*;
036import static org.opends.server.util.StaticUtils.*;
037
038/**
039 * This class defines a data structure that represents the components
040 * of an LDAP URL, including the scheme, host, port, base DN,
041 * attributes, scope, filter, and extensions.  It has the ability to
042 * create an LDAP URL based on all of these individual components, as
043 * well as parsing them from their string representations.
044 */
045@org.opends.server.types.PublicAPI(
046     stability=org.opends.server.types.StabilityLevel.UNCOMMITTED,
047     mayInstantiate=true,
048     mayExtend=false,
049     mayInvoke=true)
050public final class LDAPURL
051{
052  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
053
054  /** The default scheme that will be used if none is provided. */
055  public static final String DEFAULT_SCHEME = "ldap";
056  /** The default port value that will be used if none is provided. */
057  private static final int DEFAULT_PORT = 389;
058  /** The default base DN that will be used if none is provided. */
059  private static final DN DEFAULT_BASE_DN = DN.rootDN();
060  /** The default search scope that will be used if none is provided. */
061  private static final SearchScope DEFAULT_SEARCH_SCOPE =
062       SearchScope.BASE_OBJECT;
063  /** The default search filter that will be used if none is provided. */
064  public static final SearchFilter DEFAULT_SEARCH_FILTER =
065       SearchFilter.createPresenceFilter(CoreSchema.getObjectClassAttributeType());
066
067
068  /** The host for this LDAP URL. */
069  private String host;
070  /** The port number for this LDAP URL. */
071  private int port;
072  /** The base DN for this LDAP URL. */
073  private DN baseDN;
074  /** The raw base DN for this LDAP URL. */
075  private String rawBaseDN;
076  /** The search scope for this LDAP URL. */
077  private SearchScope scope;
078  /** The search filter for this LDAP URL. */
079  private SearchFilter filter;
080  /** The raw filter for this LDAP URL. */
081  private String rawFilter;
082
083  /** The set of attributes for this LDAP URL. */
084  private final LinkedHashSet<String> attributes;
085  /** The set of extensions for this LDAP URL. */
086  private final List<String> extensions;
087
088
089  /** The scheme (i.e., protocol) for this LDAP URL. */
090  private String scheme;
091
092
093
094  /**
095   * Creates a new LDAP URL with the provided information.
096   *
097   * @param  scheme      The scheme (i.e., protocol) for this LDAP
098   *                     URL.
099   * @param  host        The address for this LDAP URL.
100   * @param  port        The port number for this LDAP URL.
101   * @param  rawBaseDN   The raw base DN for this LDAP URL.
102   * @param  attributes  The set of requested attributes for this LDAP
103   *                     URL.
104   * @param  scope       The search scope for this LDAP URL.
105   * @param  rawFilter   The string representation of the search
106   *                     filter for this LDAP URL.
107   * @param  extensions  The set of extensions for this LDAP URL.
108   */
109  public LDAPURL(String scheme, String host, int port,
110                 String rawBaseDN, LinkedHashSet<String> attributes,
111                 SearchScope scope, String rawFilter,
112                 List<String> extensions)
113  {
114    this.host = toLowerCase(host);
115
116    baseDN = null;
117    filter = null;
118
119
120    if (scheme == null)
121    {
122      this.scheme = "ldap";
123    }
124    else
125    {
126      this.scheme = toLowerCase(scheme);
127    }
128
129    this.port = toPort(port);
130
131    if (rawBaseDN == null)
132    {
133      this.rawBaseDN = "";
134    }
135    else
136    {
137      this.rawBaseDN = rawBaseDN;
138    }
139
140    if (attributes == null)
141    {
142      this.attributes = new LinkedHashSet<>();
143    }
144    else
145    {
146      this.attributes = attributes;
147    }
148
149    if (scope == null)
150    {
151      this.scope = DEFAULT_SEARCH_SCOPE;
152    }
153    else
154    {
155      this.scope = scope;
156    }
157
158    if (rawFilter != null)
159    {
160      this.rawFilter = rawFilter;
161    }
162    else
163    {
164      setFilter(SearchFilter.objectClassPresent());
165    }
166
167    if (extensions == null)
168    {
169      this.extensions = new LinkedList<>();
170    }
171    else
172    {
173      this.extensions = extensions;
174    }
175  }
176
177
178
179  /**
180   * Creates a new LDAP URL with the provided information.
181   *
182   * @param  scheme      The scheme (i.e., protocol) for this LDAP
183   *                     URL.
184   * @param  host        The address for this LDAP URL.
185   * @param  port        The port number for this LDAP URL.
186   * @param  baseDN      The base DN for this LDAP URL.
187   * @param  attributes  The set of requested attributes for this LDAP
188   *                     URL.
189   * @param  scope       The search scope for this LDAP URL.
190   * @param  filter      The search filter for this LDAP URL.
191   * @param  extensions  The set of extensions for this LDAP URL.
192   */
193  public LDAPURL(String scheme, String host, int port, DN baseDN,
194                 LinkedHashSet<String> attributes, SearchScope scope,
195                 SearchFilter filter, List<String> extensions)
196  {
197    this.host = toLowerCase(host);
198
199
200    if (scheme == null)
201    {
202      this.scheme = "ldap";
203    }
204    else
205    {
206      this.scheme = toLowerCase(scheme);
207    }
208
209    this.port = toPort(port);
210
211    if (baseDN == null)
212    {
213      this.baseDN    = DEFAULT_BASE_DN;
214      this.rawBaseDN = DEFAULT_BASE_DN.toString();
215    }
216    else
217    {
218      this.baseDN    = baseDN;
219      this.rawBaseDN = baseDN.toString();
220    }
221
222    if (attributes == null)
223    {
224      this.attributes = new LinkedHashSet<>();
225    }
226    else
227    {
228      this.attributes = attributes;
229    }
230
231    if (scope == null)
232    {
233      this.scope = DEFAULT_SEARCH_SCOPE;
234    }
235    else
236    {
237      this.scope = scope;
238    }
239
240    if (filter == null)
241    {
242      this.filter    = DEFAULT_SEARCH_FILTER;
243      this.rawFilter = DEFAULT_SEARCH_FILTER.toString();
244    }
245    else
246    {
247      this.filter    = filter;
248      this.rawFilter = filter.toString();
249    }
250
251    if (extensions == null)
252    {
253      this.extensions = new LinkedList<>();
254    }
255    else
256    {
257      this.extensions = extensions;
258    }
259  }
260
261
262
263  /**
264   * Decodes the provided string as an LDAP URL.
265   *
266   * @param  url          The URL string to be decoded.
267   * @param  fullyDecode  Indicates whether the URL should be fully
268   *                      decoded (e.g., parsing the base DN and
269   *                      search filter) or just leaving them in their
270   *                      string representations.  The latter may be
271   *                      required for client-side use.
272   *
273   * @return  The LDAP URL decoded from the provided string.
274   *
275   * @throws  DirectoryException  If a problem occurs while attempting
276   *                              to decode the provided string as an
277   *                              LDAP URL.
278   */
279  public static LDAPURL decode(String url, boolean fullyDecode)
280         throws DirectoryException
281  {
282    // Find the "://" component, which will separate the scheme from
283    // the host.
284    String scheme;
285    int schemeEndPos = url.indexOf("://");
286    if (schemeEndPos < 0)
287    {
288      LocalizableMessage message = ERR_LDAPURL_NO_COLON_SLASH_SLASH.get(url);
289      throw new DirectoryException(INVALID_ATTRIBUTE_SYNTAX, message);
290    }
291    else if (schemeEndPos == 0)
292    {
293      LocalizableMessage message = ERR_LDAPURL_NO_SCHEME.get(url);
294      throw new DirectoryException(INVALID_ATTRIBUTE_SYNTAX, message);
295    }
296    else
297    {
298      scheme = urlDecode(url.substring(0, schemeEndPos));
299      // FIXME also need to check that the scheme is actually ldap/ldaps!!
300    }
301
302
303    // If the "://" was the end of the URL, then we're done.
304    int length = url.length();
305    if (length == schemeEndPos+3)
306    {
307      return new LDAPURL(scheme, null, DEFAULT_PORT, DEFAULT_BASE_DN,
308                         null, DEFAULT_SEARCH_SCOPE,
309                         DEFAULT_SEARCH_FILTER, null);
310    }
311
312
313    // Look at the next character.  If it's anything but a slash, then
314    // it should be part of the host and optional port.
315    String host     = null;
316    int    port     = DEFAULT_PORT;
317    int    startPos = schemeEndPos + 3;
318    int    pos      = startPos;
319    while (pos < length)
320    {
321      char c = url.charAt(pos);
322      if (c == '/')
323      {
324        break;
325      }
326      pos++;
327    }
328
329    if (pos > startPos)
330    {
331      String hostPort = url.substring(startPos, pos);
332      int colonPos = hostPort.lastIndexOf(':');
333      if (colonPos < 0)
334      {
335        host = urlDecode(hostPort);
336      }
337      else if (colonPos == 0)
338      {
339        LocalizableMessage message = ERR_LDAPURL_NO_HOST.get(url);
340        throw new DirectoryException(INVALID_ATTRIBUTE_SYNTAX, message);
341      }
342      else if (colonPos == (hostPort.length() - 1))
343      {
344        LocalizableMessage message = ERR_LDAPURL_NO_PORT.get(url);
345        throw new DirectoryException(INVALID_ATTRIBUTE_SYNTAX, message);
346      }
347      else
348      {
349        try
350        {
351          final HostPort hp = HostPort.valueOf(hostPort);
352          host = urlDecode(hp.getHost());
353          port = hp.getPort();
354        }
355        catch (NumberFormatException e)
356        {
357          LocalizableMessage message = ERR_LDAPURL_CANNOT_DECODE_PORT.get(
358              url, hostPort.substring(colonPos+1));
359          throw new DirectoryException(INVALID_ATTRIBUTE_SYNTAX, message);
360        }
361        catch (IllegalArgumentException e)
362        {
363          LocalizableMessage message = ERR_LDAPURL_INVALID_PORT.get(url, port);
364          throw new DirectoryException(INVALID_ATTRIBUTE_SYNTAX, message);
365        }
366      }
367    }
368
369
370    // Move past the slash.  If we're at or past the end of the
371    // string, then we're done.
372    pos++;
373    if (pos > length)
374    {
375      return new LDAPURL(scheme, host, port, DEFAULT_BASE_DN, null,
376                         DEFAULT_SEARCH_SCOPE, DEFAULT_SEARCH_FILTER,
377                         null);
378    }
379    startPos = pos;
380
381
382    // The next delimiter should be a question mark.  If there isn't
383    // one, then the rest of the value must be the base DN.
384    String baseDNString = null;
385    pos = url.indexOf('?', startPos);
386    if (pos < 0)
387    {
388      baseDNString = urlDecode(url.substring(startPos));
389      startPos = length;
390    }
391    else
392    {
393      baseDNString = urlDecode(url.substring(startPos, pos));
394      startPos = pos+1;
395    }
396
397    DN baseDN;
398    if (fullyDecode)
399    {
400      baseDN = DN.valueOf(baseDNString);
401    }
402    else
403    {
404      baseDN = null;
405    }
406
407
408    if (startPos >= length)
409    {
410      if (fullyDecode)
411      {
412        return new LDAPURL(scheme, host, port, baseDN, null,
413                           DEFAULT_SEARCH_SCOPE,
414                           DEFAULT_SEARCH_FILTER, null);
415      }
416      else
417      {
418        return new LDAPURL(scheme, host, port, baseDNString, null,
419                           DEFAULT_SEARCH_SCOPE, null, null);
420      }
421    }
422
423
424    // Find the next question mark (or the end of the string if there
425    // aren't any more) and get the attribute list from it.
426    String attrsString;
427    pos = url.indexOf('?', startPos);
428    if (pos < 0)
429    {
430      attrsString = url.substring(startPos);
431      startPos = length;
432    }
433    else
434    {
435      attrsString = url.substring(startPos, pos);
436      startPos = pos+1;
437    }
438
439    LinkedHashSet<String> attributes = new LinkedHashSet<>();
440    StringTokenizer tokenizer = new StringTokenizer(attrsString, ",");
441    while (tokenizer.hasMoreTokens())
442    {
443      attributes.add(urlDecode(tokenizer.nextToken()));
444    }
445
446    if (startPos >= length)
447    {
448      if (fullyDecode)
449      {
450        return new LDAPURL(scheme, host, port, baseDN, attributes,
451                           DEFAULT_SEARCH_SCOPE,
452                           DEFAULT_SEARCH_FILTER, null);
453      }
454      else
455      {
456        return new LDAPURL(scheme, host, port, baseDNString,
457                           attributes, DEFAULT_SEARCH_SCOPE, null,
458                           null);
459      }
460    }
461
462
463    // Find the next question mark (or the end of the string if there
464    // aren't any more) and get the scope from it.
465    String scopeString;
466    pos = url.indexOf('?', startPos);
467    if (pos < 0)
468    {
469      scopeString = toLowerCase(urlDecode(url.substring(startPos)));
470      startPos = length;
471    }
472    else
473    {
474      scopeString =
475           toLowerCase(urlDecode(url.substring(startPos, pos)));
476      startPos = pos+1;
477    }
478
479    SearchScope scope;
480    if (scopeString.equals(""))
481    {
482      scope = DEFAULT_SEARCH_SCOPE;
483    }
484    else if (scopeString.equals("base"))
485    {
486      scope = SearchScope.BASE_OBJECT;
487    }
488    else if (scopeString.equals("one"))
489    {
490      scope = SearchScope.SINGLE_LEVEL;
491    }
492    else if (scopeString.equals("sub"))
493    {
494      scope = SearchScope.WHOLE_SUBTREE;
495    }
496    else if (scopeString.equals("subord") ||
497             scopeString.equals("subordinate"))
498    {
499      scope = SearchScope.SUBORDINATES;
500    }
501    else
502    {
503      LocalizableMessage message = ERR_LDAPURL_INVALID_SCOPE_STRING.get(url, scopeString);
504      throw new DirectoryException(
505                     ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
506    }
507
508    if (startPos >= length)
509    {
510      if (fullyDecode)
511      {
512        return new LDAPURL(scheme, host, port, baseDN, attributes,
513                           scope, DEFAULT_SEARCH_FILTER, null);
514      }
515      else
516      {
517        return new LDAPURL(scheme, host, port, baseDNString,
518                           attributes, scope, null, null);
519      }
520    }
521
522
523    // Find the next question mark (or the end of the string if there
524    // aren't any more) and get the filter from it.
525    String filterString;
526    pos = url.indexOf('?', startPos);
527    if (pos < 0)
528    {
529      filterString = urlDecode(url.substring(startPos));
530      startPos = length;
531    }
532    else
533    {
534      filterString = urlDecode(url.substring(startPos, pos));
535      startPos = pos+1;
536    }
537
538    SearchFilter filter;
539    if (fullyDecode)
540    {
541      if (filterString.equals(""))
542      {
543        filter = DEFAULT_SEARCH_FILTER;
544      }
545      else
546      {
547        filter = SearchFilter.createFilterFromString(filterString);
548      }
549
550      if (startPos >= length)
551      {
552        if (fullyDecode)
553        {
554          return new LDAPURL(scheme, host, port, baseDN, attributes,
555                             scope, filter, null);
556        }
557        else
558        {
559          return new LDAPURL(scheme, host, port, baseDNString,
560                             attributes, scope, filterString, null);
561        }
562      }
563    }
564    else
565    {
566      filter = null;
567    }
568
569
570    // The rest of the string must be the set of extensions.
571    String extensionsString = url.substring(startPos);
572    LinkedList<String> extensions = new LinkedList<>();
573    tokenizer = new StringTokenizer(extensionsString, ",");
574    while (tokenizer.hasMoreTokens())
575    {
576      extensions.add(urlDecode(tokenizer.nextToken()));
577    }
578
579
580    if (fullyDecode)
581    {
582      return new LDAPURL(scheme, host, port, baseDN, attributes,
583                         scope, filter, extensions);
584    }
585    else
586    {
587      return new LDAPURL(scheme, host, port, baseDNString, attributes,
588                         scope, filterString, extensions);
589    }
590  }
591
592
593
594  /**
595   * Converts the provided string to a form that has decoded "special"
596   * characters that have been encoded for use in an LDAP URL.
597   *
598   * @param  s  The string to be decoded.
599   *
600   * @return  The decoded string.
601   *
602   * @throws  DirectoryException  If a problem occurs while attempting
603   *                              to decode the contents of the
604   *                              provided string.
605   */
606  static String urlDecode(String s) throws DirectoryException
607  {
608    if (s == null)
609    {
610      return "";
611    }
612
613    byte[] stringBytes  = getBytes(s);
614    int    length       = stringBytes.length;
615    byte[] decodedBytes = new byte[length];
616    int    pos          = 0;
617
618    for (int i=0; i < length; i++)
619    {
620      if (stringBytes[i] == '%')
621      {
622        // There must be at least two bytes left.  If not, then that's
623        // a problem.
624        if (i+2 > length)
625        {
626          LocalizableMessage message = ERR_LDAPURL_PERCENT_TOO_CLOSE_TO_END.get(s, i);
627          throw new DirectoryException(
628                        ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
629        }
630
631        byte b;
632        switch (stringBytes[++i])
633        {
634          case '0':
635            b = (byte) 0x00;
636            break;
637          case '1':
638            b = (byte) 0x10;
639            break;
640          case '2':
641            b = (byte) 0x20;
642            break;
643          case '3':
644            b = (byte) 0x30;
645            break;
646          case '4':
647            b = (byte) 0x40;
648            break;
649          case '5':
650            b = (byte) 0x50;
651            break;
652          case '6':
653            b = (byte) 0x60;
654            break;
655          case '7':
656            b = (byte) 0x70;
657            break;
658          case '8':
659            b = (byte) 0x80;
660            break;
661          case '9':
662            b = (byte) 0x90;
663            break;
664          case 'a':
665          case 'A':
666            b = (byte) 0xA0;
667            break;
668          case 'b':
669          case 'B':
670            b = (byte) 0xB0;
671            break;
672          case 'c':
673          case 'C':
674            b = (byte) 0xC0;
675            break;
676          case 'd':
677          case 'D':
678            b = (byte) 0xD0;
679            break;
680          case 'e':
681          case 'E':
682            b = (byte) 0xE0;
683            break;
684          case 'f':
685          case 'F':
686            b = (byte) 0xF0;
687            break;
688          default:
689            LocalizableMessage message = ERR_LDAPURL_INVALID_HEX_BYTE.get(s, i);
690            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
691        }
692
693        switch (stringBytes[++i])
694        {
695          case '0':
696            break;
697          case '1':
698            b |= 0x01;
699            break;
700          case '2':
701            b |= 0x02;
702            break;
703          case '3':
704            b |= 0x03;
705            break;
706          case '4':
707            b |= 0x04;
708            break;
709          case '5':
710            b |= 0x05;
711            break;
712          case '6':
713            b |= 0x06;
714            break;
715          case '7':
716            b |= 0x07;
717            break;
718          case '8':
719            b |= 0x08;
720            break;
721          case '9':
722            b |= 0x09;
723            break;
724          case 'a':
725          case 'A':
726            b |= 0x0A;
727            break;
728          case 'b':
729          case 'B':
730            b |= 0x0B;
731            break;
732          case 'c':
733          case 'C':
734            b |= 0x0C;
735            break;
736          case 'd':
737          case 'D':
738            b |= 0x0D;
739            break;
740          case 'e':
741          case 'E':
742            b |= 0x0E;
743            break;
744          case 'f':
745          case 'F':
746            b |= 0x0F;
747            break;
748          default:
749            LocalizableMessage message = ERR_LDAPURL_INVALID_HEX_BYTE.get(s, i);
750            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
751        }
752
753        decodedBytes[pos++] = b;
754      }
755      else
756      {
757        decodedBytes[pos++] = stringBytes[i];
758      }
759    }
760
761    try
762    {
763      return new String(decodedBytes, 0, pos, "UTF-8");
764    }
765    catch (Exception e)
766    {
767      logger.traceException(e);
768
769      // This should never happen.
770      LocalizableMessage message = ERR_LDAPURL_CANNOT_CREATE_UTF8_STRING.get(
771          getExceptionMessage(e));
772      throw new DirectoryException(
773                     ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
774    }
775  }
776
777
778
779  /**
780   * Encodes the provided string portion for inclusion in an LDAP URL
781   * and appends it to the provided buffer.
782   *
783   * @param  s            The string portion to be encoded.
784   * @param  isExtension  Indicates whether the provided component is
785   *                      an extension and therefore needs to have
786   *                      commas encoded.
787   * @param  buffer       The buffer to which the information should
788   *                      be appended.
789   */
790  private static void urlEncode(String s, boolean isExtension,
791                                StringBuilder buffer)
792  {
793    if (s == null)
794    {
795      return;
796    }
797
798    int length = s.length();
799
800    for (int i=0; i < length; i++)
801    {
802      char c = s.charAt(i);
803      if (isAlpha(c) || isDigit(c))
804      {
805        buffer.append(c);
806        continue;
807      }
808
809      if (c == ',')
810      {
811        if (isExtension)
812        {
813          hexEncode(c, buffer);
814        }
815        else
816        {
817          buffer.append(c);
818        }
819
820        continue;
821      }
822
823      switch (c)
824      {
825        case '-':
826        case '.':
827        case '_':
828        case '~':
829        case ':':
830        case '/':
831        case '#':
832        case '[':
833        case ']':
834        case '@':
835        case '!':
836        case '$':
837        case '&':
838        case '\'':
839        case '(':
840        case ')':
841        case '*':
842        case '+':
843        case ';':
844        case '=':
845          buffer.append(c);
846          break;
847        default:
848          hexEncode(c, buffer);
849          break;
850      }
851    }
852  }
853
854
855
856  /**
857   * Appends a percent-encoded representation of the provided
858   * character to the given buffer.
859   *
860   * @param  c       The character to add to the buffer.
861   * @param  buffer  The buffer to which the percent-encoded
862   *                 representation should be written.
863   */
864  private static void hexEncode(char c, StringBuilder buffer)
865  {
866    if ((c & (byte) 0xFF) == c)
867    {
868      // It's a single byte.
869      buffer.append('%');
870      buffer.append(byteToHex((byte) c));
871    }
872    else
873    {
874      // It requires two bytes, and each should be prefixed by a
875      // percent sign.
876      buffer.append('%');
877      byte b1 = (byte) ((c >>> 8) & 0xFF);
878      buffer.append(byteToHex(b1));
879
880      buffer.append('%');
881      byte b2 = (byte) (c & 0xFF);
882      buffer.append(byteToHex(b2));
883    }
884  }
885
886
887
888  /**
889   * Retrieves the scheme for this LDAP URL.
890   *
891   * @return  The scheme for this LDAP URL.
892   */
893  public String getScheme()
894  {
895    return scheme;
896  }
897
898
899
900  /**
901   * Specifies the scheme for this LDAP URL.
902   *
903   * @param  scheme  The scheme for this LDAP URL.
904   */
905  public void setScheme(String scheme)
906  {
907    if (scheme == null)
908    {
909      this.scheme = DEFAULT_SCHEME;
910    }
911    else
912    {
913      this.scheme = scheme;
914    }
915  }
916
917
918
919  /**
920   * Retrieves the host for this LDAP URL.
921   *
922   * @return  The host for this LDAP URL, or <CODE>null</CODE> if none
923   *          was provided.
924   */
925  public String getHost()
926  {
927    return host;
928  }
929
930
931
932  /**
933   * Specifies the host for this LDAP URL.
934   *
935   * @param  host  The host for this LDAP URL.
936   */
937  public void setHost(String host)
938  {
939    this.host = host;
940  }
941
942
943
944  /**
945   * Retrieves the port for this LDAP URL.
946   *
947   * @return  The port for this LDAP URL.
948   */
949  public int getPort()
950  {
951    return port;
952  }
953
954
955
956  /**
957   * Specifies the port for this LDAP URL.
958   *
959   * @param  port  The port for this LDAP URL.
960   */
961  public void setPort(int port)
962  {
963    this.port = toPort(port);
964  }
965
966  private int toPort(int port)
967  {
968    if (0 < port && port <= 65535)
969    {
970      return port;
971    }
972    return DEFAULT_PORT;
973  }
974
975  /**
976   * Retrieve the raw, unprocessed base DN for this LDAP URL.
977   *
978   * @return  The raw, unprocessed base DN for this LDAP URL, or
979   *          <CODE>null</CODE> if none was given (in which case a
980   *          default of the null DN "" should be assumed).
981   */
982  public String getRawBaseDN()
983  {
984    return rawBaseDN;
985  }
986
987
988
989  /**
990   * Specifies the raw, unprocessed base DN for this LDAP URL.
991   *
992   * @param  rawBaseDN  The raw, unprocessed base DN for this LDAP
993   *                    URL.
994   */
995  public void setRawBaseDN(String rawBaseDN)
996  {
997    this.rawBaseDN = rawBaseDN;
998    this.baseDN    = null;
999  }
1000
1001
1002
1003  /**
1004   * Retrieves the processed DN for this LDAP URL.
1005   *
1006   * @return  The processed DN for this LDAP URL.
1007   *
1008   * @throws  DirectoryException  If the raw base DN cannot be decoded
1009   *                              as a valid DN.
1010   */
1011  public DN getBaseDN()
1012         throws DirectoryException
1013  {
1014    if (baseDN == null)
1015    {
1016      if (rawBaseDN == null || rawBaseDN.length() == 0)
1017      {
1018        return DEFAULT_BASE_DN;
1019      }
1020
1021      baseDN = DN.valueOf(rawBaseDN);
1022    }
1023
1024    return baseDN;
1025  }
1026
1027
1028
1029  /**
1030   * Specifies the base DN for this LDAP URL.
1031   *
1032   * @param  baseDN  The base DN for this LDAP URL.
1033   */
1034  public void setBaseDN(DN baseDN)
1035  {
1036    if (baseDN == null)
1037    {
1038      this.baseDN    = null;
1039      this.rawBaseDN = null;
1040    }
1041    else
1042    {
1043      this.baseDN    = baseDN;
1044      this.rawBaseDN = baseDN.toString();
1045    }
1046  }
1047
1048
1049
1050  /**
1051   * Retrieves the set of attributes for this LDAP URL.  The contents
1052   * of the returned set may be altered by the caller.
1053   *
1054   * @return  The set of attributes for this LDAP URL.
1055   */
1056  public LinkedHashSet<String> getAttributes()
1057  {
1058    return attributes;
1059  }
1060
1061
1062
1063  /**
1064   * Retrieves the search scope for this LDAP URL.
1065   *
1066   * @return  The search scope for this LDAP URL, or <CODE>null</CODE>
1067   *          if none was given (in which case the base-level scope
1068   *          should be assumed).
1069   */
1070  public SearchScope getScope()
1071  {
1072    return scope;
1073  }
1074
1075
1076
1077  /**
1078   * Specifies the search scope for this LDAP URL.
1079   *
1080   * @param  scope  The search scope for this LDAP URL.
1081   */
1082  public void setScope(SearchScope scope)
1083  {
1084    if (scope == null)
1085    {
1086      this.scope = DEFAULT_SEARCH_SCOPE;
1087    }
1088    else
1089    {
1090      this.scope = scope;
1091    }
1092  }
1093
1094
1095
1096  /**
1097   * Retrieves the raw, unprocessed search filter for this LDAP URL.
1098   *
1099   * @return  The raw, unprocessed search filter for this LDAP URL, or
1100   *          <CODE>null</CODE> if none was given (in which case a
1101   *          default filter of "(objectClass=*)" should be assumed).
1102   */
1103  public String getRawFilter()
1104  {
1105    return rawFilter;
1106  }
1107
1108
1109
1110  /**
1111   * Specifies the raw, unprocessed search filter for this LDAP URL.
1112   *
1113   * @param  rawFilter  The raw, unprocessed search filter for this
1114   *                    LDAP URL.
1115   */
1116  public void setRawFilter(String rawFilter)
1117  {
1118    this.rawFilter = rawFilter;
1119    this.filter    = null;
1120  }
1121
1122
1123
1124  /**
1125   * Retrieves the processed search filter for this LDAP URL.
1126   *
1127   * @return  The processed search filter for this LDAP URL.
1128   *
1129   * @throws  DirectoryException  If a problem occurs while attempting
1130   *                              to decode the raw filter.
1131   */
1132  public SearchFilter getFilter()
1133         throws DirectoryException
1134  {
1135    if (filter == null)
1136    {
1137      if (rawFilter == null)
1138      {
1139        filter = DEFAULT_SEARCH_FILTER;
1140      }
1141      else
1142      {
1143        filter = SearchFilter.createFilterFromString(rawFilter);
1144      }
1145    }
1146
1147    return filter;
1148  }
1149
1150
1151
1152  /**
1153   * Specifies the search filter for this LDAP URL.
1154   *
1155   * @param  filter  The search filter for this LDAP URL.
1156   */
1157  public void setFilter(SearchFilter filter)
1158  {
1159    if (filter == null)
1160    {
1161      this.rawFilter = null;
1162      this.filter    = null;
1163    }
1164    else
1165    {
1166      this.rawFilter = filter.toString();
1167      this.filter    = filter;
1168    }
1169  }
1170
1171
1172
1173  /**
1174   * Retrieves the set of extensions for this LDAP URL.  The contents
1175   * of the returned list may be altered by the caller.
1176   *
1177   * @return  The set of extensions for this LDAP URL.
1178   */
1179  public List<String> getExtensions()
1180  {
1181    return extensions;
1182  }
1183
1184
1185
1186  /**
1187   * Indicates whether the provided entry matches the criteria defined
1188   * in this LDAP URL.
1189   *
1190   * @param  entry  The entry for which to make the determination.
1191   *
1192   * @return  {@code true} if the provided entry does match the
1193   *          criteria specified in this LDAP URL, or {@code false} if
1194   *          it does not.
1195   *
1196   * @throws  DirectoryException  If a problem occurs while attempting
1197   *                              to make the determination.
1198   */
1199  public boolean matchesEntry(Entry entry)
1200         throws DirectoryException
1201  {
1202    SearchScope scope = getScope();
1203    if (scope == null)
1204    {
1205      scope = SearchScope.BASE_OBJECT;
1206    }
1207
1208    return entry.matchesBaseAndScope(getBaseDN(), scope)
1209        && getFilter().matchesEntry(entry);
1210  }
1211
1212
1213
1214  /**
1215   * Indicates whether the provided object is equal to this LDAP URL.
1216   *
1217   * @param  o  The object for which to make the determination.
1218   *
1219   * @return  <CODE>true</CODE> if the object is equal to this LDAP
1220   *          URL, or <CODE>false</CODE> if not.
1221   */
1222  @Override
1223  public boolean equals(Object o)
1224  {
1225    if (o == this)
1226    {
1227      return true;
1228    }
1229    if (!(o instanceof LDAPURL))
1230    {
1231      return false;
1232    }
1233
1234    LDAPURL url = (LDAPURL) o;
1235    return scheme.equals(url.getScheme())
1236        && hostEquals(url)
1237        && port == url.getPort()
1238        && baseDnsEqual(url)
1239        && scope.equals(url.getScope())
1240        && filtersEqual(url)
1241        && attributesEqual(url.getAttributes())
1242        && extensionsEqual(url.getExtensions());
1243  }
1244
1245  private boolean hostEquals(LDAPURL url)
1246  {
1247    if (host != null)
1248    {
1249      return host.equalsIgnoreCase(url.getHost());
1250    }
1251    return url.getHost() == null;
1252  }
1253
1254  private boolean baseDnsEqual(LDAPURL url)
1255  {
1256    try
1257    {
1258      return getBaseDN().equals(url.getBaseDN());
1259    }
1260    catch (Exception e)
1261    {
1262      logger.traceException(e);
1263      return Objects.equals(rawBaseDN, url.getRawBaseDN());
1264    }
1265  }
1266
1267  private boolean filtersEqual(LDAPURL url)
1268  {
1269    try
1270    {
1271      return getFilter().equals(url.getFilter());
1272    }
1273    catch (Exception e)
1274    {
1275      logger.traceException(e);
1276      return Objects.equals(rawFilter, url.getRawFilter());
1277    }
1278  }
1279
1280  private boolean attributesEqual(Set<String> urlAttrs)
1281  {
1282    if (attributes.size() != urlAttrs.size())
1283    {
1284      return false;
1285    }
1286
1287    for (String attr : attributes)
1288    {
1289      if (!urlAttrs.contains(attr) && !containsIgnoreCase(urlAttrs, attr))
1290      {
1291        return false;
1292      }
1293    }
1294    return true;
1295  }
1296
1297  private boolean containsIgnoreCase(Set<String> urlAttrs, String attr)
1298  {
1299    for (String attr2 : urlAttrs)
1300    {
1301      if (attr.equalsIgnoreCase(attr2))
1302      {
1303        return true;
1304      }
1305    }
1306    return false;
1307  }
1308
1309  private boolean extensionsEqual(List<String> extensions)
1310  {
1311    if (this.extensions.size() != extensions.size())
1312    {
1313      return false;
1314    }
1315
1316    for (String ext : this.extensions)
1317    {
1318      if (!extensions.contains(ext))
1319      {
1320        return false;
1321      }
1322    }
1323    return true;
1324  }
1325
1326
1327
1328  /**
1329   * Retrieves the hash code for this LDAP URL.
1330   *
1331   * @return  The hash code for this LDAP URL.
1332   */
1333  @Override
1334  public int hashCode()
1335  {
1336    int hashCode = 0;
1337
1338    hashCode += scheme.hashCode();
1339
1340    if (host != null)
1341    {
1342      hashCode += toLowerCase(host).hashCode();
1343    }
1344
1345    hashCode += port;
1346
1347    try
1348    {
1349      hashCode += getBaseDN().hashCode();
1350    }
1351    catch (Exception e)
1352    {
1353      logger.traceException(e);
1354
1355      if (rawBaseDN != null)
1356      {
1357        hashCode += rawBaseDN.hashCode();
1358      }
1359    }
1360
1361    hashCode += getScope().intValue();
1362
1363    for (String attr : attributes)
1364    {
1365      hashCode += toLowerCase(attr).hashCode();
1366    }
1367
1368    try
1369    {
1370      hashCode += getFilter().hashCode();
1371    }
1372    catch (Exception e)
1373    {
1374      logger.traceException(e);
1375
1376      if (rawFilter != null)
1377      {
1378        hashCode += rawFilter.hashCode();
1379      }
1380    }
1381
1382    for (String ext : extensions)
1383    {
1384      hashCode += ext.hashCode();
1385    }
1386
1387    return hashCode;
1388  }
1389
1390
1391
1392  /**
1393   * Retrieves a string representation of this LDAP URL.
1394   *
1395   * @return  A string representation of this LDAP URL.
1396   */
1397  @Override
1398  public String toString()
1399  {
1400    StringBuilder buffer = new StringBuilder();
1401    toString(buffer, false);
1402    return buffer.toString();
1403  }
1404
1405
1406
1407  /**
1408   * Appends a string representation of this LDAP URL to the provided
1409   * buffer.
1410   *
1411   * @param  buffer    The buffer to which the information is to be
1412   *                   appended.
1413   * @param  baseOnly  Indicates whether the resulting URL string
1414   *                   should only include the portion up to the base
1415   *                   DN, omitting the attributes, scope, filter, and
1416   *                   extensions.
1417   */
1418  public void toString(StringBuilder buffer, boolean baseOnly)
1419  {
1420    urlEncode(scheme, false, buffer);
1421    buffer.append("://");
1422
1423    if (host != null)
1424    {
1425      urlEncode(host, false, buffer);
1426      buffer.append(":");
1427      buffer.append(port);
1428    }
1429
1430    buffer.append("/");
1431    urlEncode(rawBaseDN, false, buffer);
1432
1433    if (baseOnly)
1434    {
1435      // If there are extensions, then we need to include them.
1436      // Technically, we only have to include critical extensions, but
1437      // we'll use all of them.
1438      if (! extensions.isEmpty())
1439      {
1440        buffer.append("????");
1441        Iterator<String> iterator = extensions.iterator();
1442        urlEncode(iterator.next(), true, buffer);
1443
1444        while (iterator.hasNext())
1445        {
1446          buffer.append(",");
1447          urlEncode(iterator.next(), true, buffer);
1448        }
1449      }
1450
1451      return;
1452    }
1453
1454    buffer.append("?");
1455    if (! attributes.isEmpty())
1456    {
1457      Iterator<String> iterator = attributes.iterator();
1458      urlEncode(iterator.next(), false, buffer);
1459
1460      while (iterator.hasNext())
1461      {
1462        buffer.append(",");
1463        urlEncode(iterator.next(), false, buffer);
1464      }
1465    }
1466
1467    buffer.append("?");
1468    switch (scope.asEnum())
1469    {
1470      case BASE_OBJECT:
1471        buffer.append("base");
1472        break;
1473      case SINGLE_LEVEL:
1474        buffer.append("one");
1475        break;
1476      case WHOLE_SUBTREE:
1477        buffer.append("sub");
1478        break;
1479      case SUBORDINATES:
1480        buffer.append("subordinate");
1481        break;
1482    }
1483
1484    buffer.append("?");
1485    urlEncode(rawFilter, false, buffer);
1486
1487    if (! extensions.isEmpty())
1488    {
1489      buffer.append("?");
1490      Iterator<String> iterator = extensions.iterator();
1491      urlEncode(iterator.next(), true, buffer);
1492
1493      while (iterator.hasNext())
1494      {
1495        buffer.append(",");
1496        urlEncode(iterator.next(), true, buffer);
1497      }
1498    }
1499  }
1500}