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