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-2010 Sun Microsystems, Inc.
015 * Portions Copyright 2011-2016 ForgeRock AS.
016 */
017package org.opends.server.types;
018
019import java.util.ArrayList;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.HashSet;
023import java.util.InputMismatchException;
024import java.util.Iterator;
025import java.util.Map;
026import java.util.NoSuchElementException;
027import java.util.Objects;
028import java.util.Scanner;
029import java.util.Set;
030import java.util.TreeMap;
031import java.util.regex.Pattern;
032
033import org.forgerock.i18n.LocalizableMessage;
034import org.forgerock.opendj.ldap.DN;
035import org.forgerock.opendj.ldap.ResultCode;
036import org.opends.server.core.DirectoryServer;
037import org.opends.server.util.StaticUtils;
038
039import static org.opends.messages.SchemaMessages.*;
040
041/**
042 * An RFC 3672 subtree specification.
043 * <p>
044 * This implementation extends RFC 3672 by supporting search filters
045 * for specification filters. More specifically, the
046 * {@code Refinement} product has been extended as follows:
047 *
048 * <pre>
049 *  Refinement = item / and / or / not / Filter
050 *
051 *  Filter     = dquote *SafeUTF8Character dquote
052 * </pre>
053 *
054 * @see <a href="http://tools.ietf.org/html/rfc3672">RFC 3672 -
055 *      Subentries in the Lightweight Directory Access Protocol (LDAP)
056 *      </a>
057 */
058@org.opends.server.types.PublicAPI(
059    stability = org.opends.server.types.StabilityLevel.VOLATILE,
060    mayInstantiate = false,
061    mayExtend = true,
062    mayInvoke = false)
063public final class SubtreeSpecification
064{
065
066  /**
067   * RFC 3672 subtree specification AND refinement. This type of
068   * refinement filters entries based on all of the underlying
069   * refinements being <code>true</code>.
070   */
071  public static final class AndRefinement extends Refinement
072  {
073    /** The set of refinements which must all be true. */
074    private final Collection<Refinement> refinementSet;
075
076    /**
077     * Create a new AND refinement.
078     *
079     * @param refinementSet
080     *          The set of refinements which must all be
081     *          <code>true</code>.
082     */
083    public AndRefinement(final Collection<Refinement> refinementSet)
084    {
085      this.refinementSet = refinementSet;
086    }
087
088    @Override
089    public boolean equals(final Object obj)
090    {
091      if (this == obj)
092      {
093        return true;
094      }
095
096      if (obj instanceof AndRefinement)
097      {
098        final AndRefinement other = (AndRefinement) obj;
099
100        return refinementSet.equals(other.refinementSet);
101      }
102
103      return false;
104    }
105
106    @Override
107    public int hashCode()
108    {
109      return refinementSet.hashCode();
110    }
111
112    @Override
113    public boolean matches(final Entry entry)
114    {
115      for (final Refinement refinement : refinementSet)
116      {
117        if (!refinement.matches(entry))
118        {
119          return false;
120        }
121      }
122
123      // All sub-refinements matched.
124      return true;
125    }
126
127    @Override
128    public StringBuilder toString(final StringBuilder builder)
129    {
130      switch (refinementSet.size())
131      {
132      case 0:
133        // Do nothing.
134        break;
135      case 1:
136        refinementSet.iterator().next().toString(builder);
137        break;
138      default:
139        builder.append("and:{");
140        final Iterator<Refinement> iterator = refinementSet
141            .iterator();
142        iterator.next().toString(builder);
143        while (iterator.hasNext())
144        {
145          builder.append(", ");
146          iterator.next().toString(builder);
147        }
148        builder.append("}");
149        break;
150      }
151
152      return builder;
153    }
154  }
155
156  /** A refinement which uses a search filter. */
157  public static final class FilterRefinement extends Refinement
158  {
159    /** The search filter. */
160    private final SearchFilter filter;
161
162    /**
163     * Create a new filter refinement.
164     *
165     * @param filter
166     *          The search filter.
167     */
168    public FilterRefinement(final SearchFilter filter)
169    {
170      this.filter = filter;
171    }
172
173    @Override
174    public boolean equals(final Object obj)
175    {
176      if (this == obj)
177      {
178        return true;
179      }
180
181      if (obj instanceof FilterRefinement)
182      {
183        final FilterRefinement other = (FilterRefinement) obj;
184        return filter.equals(other.filter);
185      }
186
187      return false;
188    }
189
190    @Override
191    public int hashCode()
192    {
193      return filter.hashCode();
194    }
195
196    @Override
197    public boolean matches(final Entry entry)
198    {
199      try
200      {
201        return filter.matchesEntry(entry);
202      }
203      catch (final DirectoryException e)
204      {
205        // TODO: need to decide what to do with the exception here.
206        // It's probably safe to ignore, but we could log it perhaps.
207        return false;
208      }
209    }
210
211    @Override
212    public StringBuilder toString(final StringBuilder builder)
213    {
214      StaticUtils.toRFC3641StringValue(builder, filter.toString());
215      return builder;
216    }
217  }
218
219  /**
220   * RFC 3672 subtree specification Item refinement. This type of
221   * refinement filters entries based on the presence of a specified
222   * object class.
223   */
224  public static final class ItemRefinement extends Refinement
225  {
226    /** The item's object class. */
227    private final String objectClass;
228
229    /** The item's normalized object class. */
230    private final String normalizedObjectClass;
231
232    /**
233     * Create a new item refinement.
234     *
235     * @param objectClass
236     *          The item's object class.
237     */
238    public ItemRefinement(final String objectClass)
239    {
240      this.objectClass = objectClass;
241      this.normalizedObjectClass = StaticUtils
242          .toLowerCase(objectClass.trim());
243    }
244
245    @Override
246    public boolean equals(final Object obj)
247    {
248      if (this == obj)
249      {
250        return true;
251      }
252
253      if (obj instanceof ItemRefinement)
254      {
255        final ItemRefinement other = (ItemRefinement) obj;
256
257        return normalizedObjectClass
258            .equals(other.normalizedObjectClass);
259      }
260
261      return false;
262    }
263
264    @Override
265    public int hashCode()
266    {
267      return normalizedObjectClass.hashCode();
268    }
269
270    @Override
271    public boolean matches(final Entry entry)
272    {
273      final ObjectClass oc = DirectoryServer.getObjectClass(normalizedObjectClass);
274      return oc != null && entry.hasObjectClass(oc);
275    }
276
277    @Override
278    public StringBuilder toString(final StringBuilder builder)
279    {
280      builder.append("item:");
281      builder.append(objectClass);
282      return builder;
283    }
284  }
285
286  /**
287   * RFC 3672 subtree specification NOT refinement. This type of
288   * refinement filters entries based on the underlying refinement
289   * being <code>false</code>
290   * .
291   */
292  public static final class NotRefinement extends Refinement
293  {
294    /** The inverted refinement. */
295    private final Refinement refinement;
296
297    /**
298     * Create a new NOT refinement.
299     *
300     * @param refinement
301     *          The refinement which must be <code>false</code>.
302     */
303    public NotRefinement(final Refinement refinement)
304    {
305      this.refinement = refinement;
306    }
307
308    @Override
309    public boolean equals(final Object obj)
310    {
311      if (this == obj)
312      {
313        return true;
314      }
315
316      if (obj instanceof NotRefinement)
317      {
318        final NotRefinement other = (NotRefinement) obj;
319
320        return refinement.equals(other.refinement);
321      }
322
323      return false;
324    }
325
326    @Override
327    public int hashCode()
328    {
329      return refinement.hashCode();
330    }
331
332    @Override
333    public boolean matches(final Entry entry)
334    {
335      return !refinement.matches(entry);
336    }
337
338    @Override
339    public StringBuilder toString(final StringBuilder builder)
340    {
341      builder.append("not:");
342      return refinement.toString(builder);
343    }
344  }
345
346  /**
347   * RFC 3672 subtree specification OR refinement. This type of
348   * refinement filters entries based on at least one of the
349   * underlying refinements being <code>true</code>.
350   */
351  public static final class OrRefinement extends Refinement
352  {
353    /** The set of refinements of which at least one must be true. */
354    private final Collection<Refinement> refinementSet;
355
356    /**
357     * Create a new OR refinement.
358     *
359     * @param refinementSet
360     *          The set of refinements of which at least one must be
361     *          <code>true</code>.
362     */
363    public OrRefinement(final Collection<Refinement> refinementSet)
364    {
365      this.refinementSet = refinementSet;
366    }
367
368    @Override
369    public boolean equals(final Object obj)
370    {
371      if (this == obj)
372      {
373        return true;
374      }
375
376      if (obj instanceof AndRefinement)
377      {
378        final AndRefinement other = (AndRefinement) obj;
379
380        return refinementSet.equals(other.refinementSet);
381      }
382
383      return false;
384    }
385
386    @Override
387    public int hashCode()
388    {
389      return refinementSet.hashCode();
390    }
391
392    @Override
393    public boolean matches(final Entry entry)
394    {
395      for (final Refinement refinement : refinementSet)
396      {
397        if (refinement.matches(entry))
398        {
399          return true;
400        }
401      }
402
403      // No sub-refinements matched.
404      return false;
405    }
406
407    @Override
408    public StringBuilder toString(final StringBuilder builder)
409    {
410      switch (refinementSet.size())
411      {
412      case 0:
413        // Do nothing.
414        break;
415      case 1:
416        refinementSet.iterator().next().toString(builder);
417        break;
418      default:
419        builder.append("or:{");
420        final Iterator<Refinement> iterator = refinementSet
421            .iterator();
422        iterator.next().toString(builder);
423        while (iterator.hasNext())
424        {
425          builder.append(", ");
426          iterator.next().toString(builder);
427        }
428        builder.append("}");
429        break;
430      }
431
432      return builder;
433    }
434  }
435
436  /** Abstract interface for RFC3672 specification filter refinements. */
437  public static abstract class Refinement
438  {
439    /** Create a new RFC3672 specification filter refinement. */
440    protected Refinement()
441    {
442      // No implementation required.
443    }
444
445    @Override
446    public abstract boolean equals(Object obj);
447
448    @Override
449    public abstract int hashCode();
450
451    /**
452     * Check if the refinement matches the given entry.
453     *
454     * @param entry
455     *          The filterable entry.
456     * @return Returns <code>true</code> if the entry matches the
457     *         refinement, or <code>false</code> otherwise.
458     */
459    public abstract boolean matches(Entry entry);
460
461    @Override
462    public final String toString()
463    {
464      final StringBuilder builder = new StringBuilder();
465
466      return toString(builder).toString();
467    }
468
469    /**
470     * Append the string representation of the refinement to the
471     * provided strin builder.
472     *
473     * @param builder
474     *          The string builder.
475     * @return The string builder.
476     */
477    public abstract StringBuilder toString(StringBuilder builder);
478  }
479
480  /**
481   * Internal utility class which can be used by sub-classes to help
482   * parse string representations.
483   */
484  protected static final class Parser
485  {
486    /** Text scanner used to parse the string value. */
487    private final Scanner scanner;
488
489    /** Pattern used to detect left braces. */
490    private static Pattern LBRACE = Pattern.compile("\\{.*");
491    /** Pattern used to parse left braces. */
492    private static Pattern LBRACE_TOKEN = Pattern.compile("\\{");
493    /** Pattern used to detect right braces. */
494    private static Pattern RBRACE = Pattern.compile("\\}.*");
495    /** Pattern used to parse right braces. */
496    private static Pattern RBRACE_TOKEN = Pattern.compile("\\}");
497    /** Pattern used to detect comma separators. */
498    private static Pattern SEP = Pattern.compile(",.*");
499    /** Pattern used to parse comma separators. */
500    private static Pattern SEP_TOKEN = Pattern.compile(",");
501    /** Pattern used to detect colon separators. */
502    private static Pattern COLON = Pattern.compile(":.*");
503    /** Pattern used to parse colon separators. */
504    private static Pattern COLON_TOKEN = Pattern.compile(":");
505    /** Pattern used to detect integer values. */
506    private static Pattern INT = Pattern.compile("\\d.*");
507    /** Pattern used to parse integer values. */
508    private static Pattern INT_TOKEN = Pattern.compile("\\d+");
509    /** Pattern used to detect name values. */
510    private static Pattern NAME = Pattern.compile("[\\w_;-].*");
511    /** Pattern used to parse name values. */
512    private static Pattern NAME_TOKEN = Pattern.compile("[\\w_;-]+");
513    /** Pattern used to detect RFC3641 string values. */
514    private static Pattern STRING_VALUE = Pattern.compile("\".*");
515    /** Pattern used to parse RFC3641 string values. */
516    private static Pattern STRING_VALUE_TOKEN = Pattern
517        .compile("\"([^\"]|(\"\"))*\"");
518
519    /**
520     * Create a new parser for a subtree specification string value.
521     *
522     * @param value
523     *          The subtree specification string value.
524     */
525    public Parser(final String value)
526    {
527      this.scanner = new Scanner(value);
528    }
529
530    /**
531     * Determine if there are remaining tokens.
532     *
533     * @return <code>true</code> if and only if there are remaining
534     *         tokens.
535     */
536    public boolean hasNext()
537    {
538      return scanner.hasNext();
539    }
540
541    /**
542     * Determine if the next token is a right-brace character.
543     *
544     * @return <code>true</code> if and only if the next token is a
545     *         valid right brace character.
546     */
547    public boolean hasNextRightBrace()
548    {
549      return scanner.hasNext(RBRACE);
550    }
551
552    /**
553     * Scans the next token of the input as a non-negative
554     * <code>int</code> value.
555     *
556     * @return The name value scanned from the input.
557     * @throws InputMismatchException
558     *           If the next token is not a valid non-negative integer
559     *           string.
560     * @throws NoSuchElementException
561     *           If input is exhausted.
562     */
563    public int nextInt() throws InputMismatchException,
564        NoSuchElementException
565    {
566      final String s = nextValue(INT, INT_TOKEN);
567      return Integer.parseInt(s);
568    }
569
570    /**
571     * Scans the next token of the input as a key value.
572     *
573     * @return The lower-case key value scanned from the input.
574     * @throws InputMismatchException
575     *           If the next token is not a valid key string.
576     * @throws NoSuchElementException
577     *           If input is exhausted.
578     */
579    public String nextKey() throws InputMismatchException,
580        NoSuchElementException
581    {
582      return StaticUtils.toLowerCase(scanner.next());
583    }
584
585    /**
586     * Scans the next token of the input as a name value.
587     * <p>
588     * A name is any string containing only alpha-numeric characters
589     * or hyphens, semi-colons, or underscores.
590     *
591     * @return The name value scanned from the input.
592     * @throws InputMismatchException
593     *           If the next token is not a valid name string.
594     * @throws NoSuchElementException
595     *           If input is exhausted.
596     */
597    public String nextName() throws InputMismatchException,
598        NoSuchElementException
599    {
600      return nextValue(NAME, NAME_TOKEN);
601    }
602
603    /**
604     * Scans the next tokens of the input as a set of specific
605     * exclusions encoded according to the SpecificExclusion
606     * production in RFC 3672.
607     *
608     * @param chopBefore
609     *          The set of chop before local names.
610     * @param chopAfter
611     *          The set of chop after local names.
612     * @throws InputMismatchException
613     *           If the common component did not have a valid syntax.
614     * @throws NoSuchElementException
615     *           If input is exhausted.
616     * @throws DirectoryException
617     *           If an error occurred when attempting to parse a
618     *           DN value.
619     */
620    public void nextSpecificExclusions(final Set<DN> chopBefore,
621        final Set<DN> chopAfter) throws InputMismatchException,
622        NoSuchElementException, DirectoryException
623    {
624      // Skip leading open-brace.
625      skipLeftBrace();
626
627      // Parse each chop DN in the sequence.
628      boolean isFirstValue = true;
629      while (true)
630      {
631        // Make sure that there is a closing brace.
632        if (hasNextRightBrace())
633        {
634          skipRightBrace();
635          break;
636        }
637
638        // Make sure that there is a comma separator if this is not
639        // the first element.
640        if (!isFirstValue)
641        {
642          skipSeparator();
643        }
644        else
645        {
646          isFirstValue = false;
647        }
648
649        // Parse each chop specification which is of the form
650        // <type>:<value>.
651        final String type = StaticUtils.toLowerCase(nextName());
652        skipColon();
653        if ("chopbefore".equals(type))
654        {
655          chopBefore.add(DN.valueOf(nextStringValue()));
656        }
657        else if ("chopafter".equals(type))
658        {
659          chopAfter.add(DN.valueOf(nextStringValue()));
660        }
661        else
662        {
663          throw new java.util.InputMismatchException();
664        }
665      }
666    }
667
668    /**
669     * Scans the next token of the input as a string quoted according
670     * to the StringValue production in RFC 3641.
671     * <p>
672     * The return string has its outer double quotes removed and any
673     * escape inner double quotes unescaped.
674     *
675     * @return The string value scanned from the input.
676     * @throws InputMismatchException
677     *           If the next token is not a valid string.
678     * @throws NoSuchElementException
679     *           If input is exhausted.
680     */
681    public String nextStringValue() throws InputMismatchException,
682        NoSuchElementException
683    {
684      final String s = nextValue(STRING_VALUE, STRING_VALUE_TOKEN);
685      return s.substring(1, s.length() - 1).replace("\"\"", "\"");
686    }
687
688    /**
689     * Skip a colon separator.
690     *
691     * @throws InputMismatchException
692     *           If the next token is not a colon separator character.
693     * @throws NoSuchElementException
694     *           If input is exhausted.
695     */
696    public void skipColon() throws InputMismatchException,
697        NoSuchElementException
698    {
699      nextValue(COLON, COLON_TOKEN);
700    }
701
702    /**
703     * Skip a left-brace character.
704     *
705     * @throws InputMismatchException
706     *           If the next token is not a left-brace character.
707     * @throws NoSuchElementException
708     *           If input is exhausted.
709     */
710    public void skipLeftBrace() throws InputMismatchException,
711        NoSuchElementException
712    {
713      nextValue(LBRACE, LBRACE_TOKEN);
714    }
715
716    /**
717     * Skip a right-brace character.
718     *
719     * @throws InputMismatchException
720     *           If the next token is not a right-brace character.
721     * @throws NoSuchElementException
722     *           If input is exhausted.
723     */
724    public void skipRightBrace() throws InputMismatchException,
725        NoSuchElementException
726    {
727      nextValue(RBRACE, RBRACE_TOKEN);
728    }
729
730    /**
731     * Skip a comma separator.
732     *
733     * @throws InputMismatchException
734     *           If the next token is not a comma separator character.
735     * @throws NoSuchElementException
736     *           If input is exhausted.
737     */
738    public void skipSeparator() throws InputMismatchException,
739        NoSuchElementException
740    {
741      nextValue(SEP, SEP_TOKEN);
742    }
743
744    /**
745     * Parse the next token from the string using the specified
746     * patterns.
747     *
748     * @param head
749     *          The pattern used to determine if the next token is a
750     *          possible match.
751     * @param content
752     *          The pattern used to parse the token content.
753     * @return The next token matching the <code>content</code>
754     *         pattern.
755     * @throws InputMismatchException
756     *           If the next token does not match the
757     *           <code>content</code> pattern.
758     * @throws NoSuchElementException
759     *           If input is exhausted.
760     */
761    private String nextValue(final Pattern head,
762        final Pattern content)
763        throws InputMismatchException, NoSuchElementException
764    {
765      if (!scanner.hasNext())
766      {
767        throw new java.util.NoSuchElementException();
768      }
769
770      if (!scanner.hasNext(head))
771      {
772        throw new java.util.InputMismatchException();
773      }
774
775      final String s = scanner.findInLine(content);
776      if (s == null)
777      {
778        throw new java.util.InputMismatchException();
779      }
780
781      return s;
782    }
783  }
784
785  /**
786   * Parses the string argument as an RFC3672 subtree specification.
787   *
788   * @param rootDN
789   *          The DN of the subtree specification's base entry.
790   * @param s
791   *          The string to be parsed.
792   * @return The RFC3672 subtree specification represented by the
793   *         string argument.
794   * @throws DirectoryException
795   *           If the string does not contain a parsable relative
796   *           subtree specification.
797   */
798  public static SubtreeSpecification valueOf(final DN rootDN,
799      final String s) throws DirectoryException
800  {
801    // Default values.
802    DN relativeBaseDN = null;
803
804    int minimum = -1;
805    int maximum = -1;
806
807    final HashSet<DN> chopBefore = new HashSet<>();
808    final HashSet<DN> chopAfter = new HashSet<>();
809
810    Refinement refinement = null;
811
812    // Value must have an opening left brace.
813    final Parser parser = new Parser(s);
814    boolean isValid = true;
815
816    try
817    {
818      parser.skipLeftBrace();
819
820      // Parse each element of the value sequence.
821      boolean isFirst = true;
822
823      while (true)
824      {
825        if (parser.hasNextRightBrace())
826        {
827          // Make sure that there is a closing brace and no trailing text.
828          parser.skipRightBrace();
829
830          if (parser.hasNext())
831          {
832            throw new java.util.InputMismatchException();
833          }
834          break;
835        }
836
837        // Make sure that there is a comma separator if this is not
838        // the first element.
839        if (!isFirst)
840        {
841          parser.skipSeparator();
842        }
843        else
844        {
845          isFirst = false;
846        }
847
848        final String key = parser.nextKey();
849        if ("base".equals(key))
850        {
851          if (relativeBaseDN != null)
852          {
853            // Relative base DN specified more than once.
854            throw new InputMismatchException();
855          }
856          relativeBaseDN = DN.valueOf(parser.nextStringValue());
857        }
858        else if ("minimum".equals(key))
859        {
860          if (minimum != -1)
861          {
862            // Minimum specified more than once.
863            throw new InputMismatchException();
864          }
865          minimum = parser.nextInt();
866        }
867        else if ("maximum".equals(key))
868        {
869          if (maximum != -1)
870          {
871            // Maximum specified more than once.
872            throw new InputMismatchException();
873          }
874          maximum = parser.nextInt();
875        }
876        else if ("specificationfilter".equals(key))
877        {
878          if (refinement != null)
879          {
880            // Refinements specified more than once.
881            throw new InputMismatchException();
882          }
883
884          // First try normal search filter before RFC3672 refinements.
885          try
886          {
887            final SearchFilter filter = SearchFilter
888                .createFilterFromString(parser.nextStringValue());
889            refinement = new FilterRefinement(filter);
890          }
891          catch (final InputMismatchException e)
892          {
893            refinement = parseRefinement(parser);
894          }
895        }
896        else if ("specificexclusions".equals(key))
897        {
898          if (!chopBefore.isEmpty() || !chopAfter.isEmpty())
899          {
900            // Specific exclusions specified more than once.
901            throw new InputMismatchException();
902          }
903
904          parser.nextSpecificExclusions(chopBefore, chopAfter);
905        }
906        else
907        {
908          throw new InputMismatchException();
909        }
910      }
911
912      // Make default minimum value is 0.
913      if (minimum < 0)
914      {
915        minimum = 0;
916      }
917
918      // Check that the maximum, if specified, is gte the minimum.
919      if (maximum >= 0 && maximum < minimum)
920      {
921        isValid = false;
922      }
923    }
924    catch (final NoSuchElementException e)
925    {
926      isValid = false;
927    }
928
929    if (!isValid)
930    {
931      final LocalizableMessage message =
932        ERR_ATTR_SYNTAX_RFC3672_SUBTREE_SPECIFICATION_INVALID.get(s);
933      throw new DirectoryException(
934          ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
935    }
936    return new SubtreeSpecification(rootDN, relativeBaseDN, minimum, maximum, chopBefore, chopAfter, refinement);
937  }
938
939  /**
940   * Parse a single refinement.
941   *
942   * @param parser
943   *          The active subtree specification parser.
944   * @return The parsed refinement.
945   * @throws InputMismatchException
946   *           If the common component did not have a valid syntax.
947   * @throws NoSuchElementException
948   *           If input is exhausted.
949   */
950  private static Refinement parseRefinement(final Parser parser)
951      throws InputMismatchException, NoSuchElementException
952  {
953    // Get the type of refinement.
954    final String type = StaticUtils.toLowerCase(parser.nextName());
955
956    // Skip the colon separator.
957    parser.skipColon();
958
959    if ("item".equals(type))
960    {
961      return new ItemRefinement(parser.nextName());
962    }
963    else if ("not".equals(type))
964    {
965      return new NotRefinement(parseRefinement(parser));
966    }
967    else if ("and".equals(type))
968    {
969      return new AndRefinement(parseRefinementSet(parser));
970    }
971    else if ("or".equals(type))
972    {
973      return new OrRefinement(parseRefinementSet(parser));
974    }
975    else
976    {
977      // Unknown refinement type.
978      throw new InputMismatchException();
979    }
980  }
981
982  /**
983   * Parse a list of refinements.
984   *
985   * @param parser
986   *          The active subtree specification parser.
987   * @return The parsed refinement list.
988   * @throws InputMismatchException
989   *           If the common component did not have a valid syntax.
990   * @throws NoSuchElementException
991   *           If input is exhausted.
992   */
993  private static ArrayList<Refinement> parseRefinementSet(
994      final Parser parser) throws InputMismatchException,
995      NoSuchElementException
996  {
997    final ArrayList<Refinement> refinements = new ArrayList<>();
998
999    // Skip leading open-brace.
1000    parser.skipLeftBrace();
1001
1002    // Parse each chop DN in the sequence.
1003    boolean isFirstValue = true;
1004    while (true)
1005    {
1006      // Make sure that there is a closing brace.
1007      if (parser.hasNextRightBrace())
1008      {
1009        parser.skipRightBrace();
1010        break;
1011      }
1012
1013      // Make sure that there is a comma separator if this is not
1014      // the first element.
1015      if (!isFirstValue)
1016      {
1017        parser.skipSeparator();
1018      }
1019      else
1020      {
1021        isFirstValue = false;
1022      }
1023
1024      // Parse each sub-refinement.
1025      refinements.add(parseRefinement(parser));
1026    }
1027
1028    return refinements;
1029  }
1030
1031  /** The absolute base of the subtree. */
1032  private final DN baseDN;
1033
1034  /** Optional minimum depth (<=0 means unlimited). */
1035  private final int minimumDepth;
1036  /** Optional maximum depth (<0 means unlimited). */
1037  private final int maximumDepth;
1038
1039  /** Optional set of chop before absolute DNs (mapping to their local-names). */
1040  private final Map<DN, DN> chopBefore;
1041
1042  /** Optional set of chop after absolute DNs (mapping to their local-names). */
1043  private final Map<DN, DN> chopAfter;
1044
1045  /** The root DN. */
1046  private final DN rootDN;
1047
1048  /** The optional relative base DN. */
1049  private final DN relativeBaseDN;
1050
1051  /** The optional specification filter refinements. */
1052  private final Refinement refinements;
1053
1054  /**
1055   * Create a new RFC3672 subtree specification.
1056   *
1057   * @param rootDN
1058   *          The root DN of the subtree.
1059   * @param relativeBaseDN
1060   *          The relative base DN (or {@code null} if not
1061   *          specified).
1062   * @param minimumDepth
1063   *          The minimum depth (less than or equal to 0 means unlimited).
1064   * @param maximumDepth
1065   *          The maximum depth (less than 0 means unlimited).
1066   * @param chopBefore
1067   *          The set of chop before local names (relative to the
1068   *          relative base DN), or {@code null} if there are
1069   *          none.
1070   * @param chopAfter
1071   *          The set of chop after local names (relative to the
1072   *          relative base DN), or {@code null} if there are
1073   *          none.
1074   * @param refinements
1075   *          The optional specification filter refinements, or
1076   *          {@code null} if there are none.
1077   */
1078  public SubtreeSpecification(final DN rootDN,
1079      final DN relativeBaseDN, final int minimumDepth,
1080      final int maximumDepth, final Iterable<DN> chopBefore,
1081      final Iterable<DN> chopAfter, final Refinement refinements)
1082  {
1083    this.baseDN = relativeBaseDN == null ? rootDN : rootDN
1084        .child(relativeBaseDN);
1085    this.minimumDepth = minimumDepth;
1086    this.maximumDepth = maximumDepth;
1087
1088    if (chopBefore != null && chopBefore.iterator().hasNext())
1089    {
1090      // Calculate the absolute DNs.
1091      final TreeMap<DN, DN> map = new TreeMap<>();
1092      for (final DN localName : chopBefore)
1093      {
1094        map.put(baseDN.child(localName), localName);
1095      }
1096      this.chopBefore = Collections.unmodifiableMap(map);
1097    }
1098    else
1099    {
1100      // No chop before specifications.
1101      this.chopBefore = Collections.emptyMap();
1102    }
1103
1104    if (chopAfter != null && chopAfter.iterator().hasNext())
1105    {
1106      // Calculate the absolute DNs.
1107      final TreeMap<DN, DN> map = new TreeMap<>();
1108      for (final DN localName : chopAfter)
1109      {
1110        map.put(baseDN.child(localName), localName);
1111      }
1112      this.chopAfter = Collections.unmodifiableMap(map);
1113    }
1114    else
1115    {
1116      // No chop after specifications.
1117      this.chopAfter = Collections.emptyMap();
1118    }
1119
1120    this.rootDN = rootDN;
1121    this.relativeBaseDN = relativeBaseDN;
1122    this.refinements = refinements;
1123  }
1124
1125  /**
1126   * Indicates whether the provided object is logically equal to this
1127   * subtree specification object.
1128   *
1129   * @param obj
1130   *          The object for which to make the determination.
1131   * @return {@code true} if the provided object is logically equal
1132   *         to this subtree specification object, or {@code false}
1133   *         if not.
1134   */
1135  @Override
1136  public boolean equals(final Object obj)
1137  {
1138    if (this == obj)
1139    {
1140      return true;
1141    }
1142
1143    if (obj instanceof SubtreeSpecification)
1144    {
1145      final SubtreeSpecification other = (SubtreeSpecification) obj;
1146
1147      return minimumDepth == other.minimumDepth
1148          && maximumDepth == other.maximumDepth
1149          && chopBefore.values().equals(other.chopBefore.values())
1150          && chopAfter.values().equals(other.chopAfter.values())
1151          && getBaseDN().equals(other.getBaseDN())
1152          && Objects.equals(refinements, other.refinements);
1153    }
1154
1155    return false;
1156  }
1157
1158  /**
1159   * Get the absolute base DN of the subtree specification.
1160   *
1161   * @return Returns the absolute base DN of the subtree
1162   *         specification.
1163   */
1164  public DN getBaseDN()
1165  {
1166    return baseDN;
1167  }
1168
1169  /**
1170   * Get the set of chop after relative DNs.
1171   *
1172   * @return Returns the set of chop after relative DNs.
1173   */
1174  public Iterable<DN> getChopAfter()
1175  {
1176    return chopAfter.values();
1177  }
1178
1179  /**
1180   * Get the set of chop before relative DNs.
1181   *
1182   * @return Returns the set of chop before relative DNs.
1183   */
1184  public Iterable<DN> getChopBefore()
1185  {
1186    return chopBefore.values();
1187  }
1188
1189  /**
1190   * Get the maximum depth of the subtree specification.
1191   *
1192   * @return Returns the maximum depth (less than 0 indicates unlimited depth).
1193   */
1194  public int getMaximumDepth()
1195  {
1196    return maximumDepth;
1197  }
1198
1199  /**
1200   * Get the minimum depth of the subtree specification.
1201   *
1202   * @return Returns the minimum depth (<=0 indicates unlimited
1203   *         depth).
1204   */
1205  public int getMinimumDepth()
1206  {
1207    return minimumDepth;
1208  }
1209
1210  /**
1211   * Get the specification filter refinements.
1212   *
1213   * @return Returns the specification filter refinements, or
1214   *         <code>null</code> if none were specified.
1215   */
1216  public Refinement getRefinements()
1217  {
1218    return refinements;
1219  }
1220
1221  /**
1222   * Get the relative base DN.
1223   *
1224   * @return Returns the relative base DN or <code>null</code> if
1225   *         none was specified.
1226   */
1227  public DN getRelativeBaseDN()
1228  {
1229    return relativeBaseDN;
1230  }
1231
1232  /**
1233   * Get the root DN.
1234   *
1235   * @return Returns the root DN.
1236   */
1237  public DN getRootDN()
1238  {
1239    return rootDN;
1240  }
1241
1242  /**
1243   * Retrieves the hash code for this subtree specification object.
1244   *
1245   * @return The hash code for this subtree specification object.
1246   */
1247  @Override
1248  public int hashCode()
1249  {
1250    int hash = minimumDepth * 31 + maximumDepth;
1251    hash = hash * 31 + chopBefore.values().hashCode();
1252    hash = hash * 31 + chopAfter.values().hashCode();
1253    hash = hash * 31 + getBaseDN().hashCode();
1254
1255    if (refinements != null)
1256    {
1257      hash = hash * 31 + refinements.hashCode();
1258    }
1259
1260    return hash;
1261  }
1262
1263  /**
1264   * Determine if the specified DN is within the scope of the subtree
1265   * specification.
1266   *
1267   * @param dn
1268   *          The distinguished name.
1269   * @return Returns <code>true</code> if the DN is within the scope
1270   *         of the subtree specification, or <code>false</code>
1271   *         otherwise.
1272   */
1273  public boolean isDNWithinScope(final DN dn)
1274  {
1275    if (!dn.isSubordinateOrEqualTo(baseDN))
1276    {
1277      return false;
1278    }
1279
1280    // Check minimum and maximum depths.
1281    final int baseRDNCount = baseDN.size();
1282
1283    if (minimumDepth > 0)
1284    {
1285      final int entryRDNCount = dn.size();
1286
1287      if (entryRDNCount - baseRDNCount < minimumDepth)
1288      {
1289        return false;
1290      }
1291    }
1292
1293    if (maximumDepth >= 0)
1294    {
1295      final int entryRDNCount = dn.size();
1296
1297      if (entryRDNCount - baseRDNCount > maximumDepth)
1298      {
1299        return false;
1300      }
1301    }
1302
1303    // Check exclusions.
1304    for (final DN chopBeforeDN : chopBefore.keySet())
1305    {
1306      if (dn.isSubordinateOrEqualTo(chopBeforeDN))
1307      {
1308        return false;
1309      }
1310    }
1311
1312    for (final DN chopAfterDN : chopAfter.keySet())
1313    {
1314      if (!dn.equals(chopAfterDN) && dn.isSubordinateOrEqualTo(chopAfterDN))
1315      {
1316        return false;
1317      }
1318    }
1319
1320    // Everything seemed to match.
1321    return true;
1322  }
1323
1324  /**
1325   * Determine if an entry is within the scope of the subtree
1326   * specification.
1327   *
1328   * @param entry
1329   *          The entry.
1330   * @return {@code true} if the entry is within the scope of the
1331   *         subtree specification, or {@code false} if not.
1332   */
1333  public boolean isWithinScope(final Entry entry)
1334  {
1335    return isDNWithinScope(entry.getName())
1336        && (refinements == null || refinements.matches(entry));
1337  }
1338
1339  /**
1340   * Retrieves a string representation of this subtree specification
1341   * object.
1342   *
1343   * @return A string representation of this subtree specification
1344   *         object.
1345   */
1346  @Override
1347  public String toString()
1348  {
1349    final StringBuilder builder = new StringBuilder();
1350    return toString(builder).toString();
1351  }
1352
1353  /**
1354   * Append the string representation of the subtree specification to
1355   * the provided string builder.
1356   *
1357   * @param builder
1358   *          The string builder.
1359   * @return The string builder.
1360   */
1361  public StringBuilder toString(final StringBuilder builder)
1362  {
1363    boolean isFirstElement = true;
1364
1365    // Output the optional base DN.
1366    builder.append("{");
1367    if (relativeBaseDN != null && !relativeBaseDN.isRootDN())
1368    {
1369      builder.append(" base ");
1370      StaticUtils.toRFC3641StringValue(builder, relativeBaseDN.toString());
1371      isFirstElement = false;
1372    }
1373
1374    // Output the optional specific exclusions.
1375    final Iterable<DN> chopBefore = getChopBefore();
1376    final Iterable<DN> chopAfter = getChopAfter();
1377
1378    if (chopBefore.iterator().hasNext()
1379        || chopAfter.iterator().hasNext())
1380    {
1381      isFirstElement = append2(builder, isFirstElement, " specificExclusions { ");
1382
1383      boolean isFirst = true;
1384
1385      for (final DN dn : chopBefore)
1386      {
1387        isFirst = append(builder, isFirst, "chopBefore:");
1388        StaticUtils.toRFC3641StringValue(builder, dn.toString());
1389      }
1390
1391      for (final DN dn : chopAfter)
1392      {
1393        isFirst = append(builder, isFirst, "chopAfter:");
1394        StaticUtils.toRFC3641StringValue(builder, dn.toString());
1395      }
1396
1397      builder.append(" }");
1398    }
1399
1400    // Output the optional minimum depth.
1401    if (getMinimumDepth() > 0)
1402    {
1403      isFirstElement = append2(builder, isFirstElement, " minimum ");
1404      builder.append(getMinimumDepth());
1405    }
1406
1407    // Output the optional maximum depth.
1408    if (getMaximumDepth() >= 0)
1409    {
1410      isFirstElement = append2(builder, isFirstElement, " maximum ");
1411      builder.append(getMaximumDepth());
1412    }
1413
1414    // Output the optional refinements.
1415    if (refinements != null)
1416    {
1417      isFirstElement = append2(builder, isFirstElement, " specificationFilter ");
1418      refinements.toString(builder);
1419    }
1420
1421    builder.append(" }");
1422
1423    return builder;
1424  }
1425
1426  private boolean append2(final StringBuilder builder, boolean isFirst, String toAppend)
1427  {
1428    if (isFirst)
1429    {
1430      isFirst = false;
1431    }
1432    else
1433    {
1434      builder.append(",");
1435    }
1436    builder.append(toAppend);
1437    return isFirst;
1438  }
1439
1440  private boolean append(final StringBuilder builder, boolean isFirst, String toAppend)
1441  {
1442    if (isFirst)
1443    {
1444      isFirst = false;
1445    }
1446    else
1447    {
1448      builder.append(", ");
1449    }
1450    builder.append(toAppend);
1451    return isFirst;
1452  }
1453}