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}