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