001/* 002 * Copyright 2007-2020 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2007-2020 Ping Identity Corporation 007 * 008 * Licensed under the Apache License, Version 2.0 (the "License"); 009 * you may not use this file except in compliance with the License. 010 * You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, software 015 * distributed under the License is distributed on an "AS IS" BASIS, 016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 017 * See the License for the specific language governing permissions and 018 * limitations under the License. 019 */ 020/* 021 * Copyright (C) 2007-2020 Ping Identity Corporation 022 * 023 * This program is free software; you can redistribute it and/or modify 024 * it under the terms of the GNU General Public License (GPLv2 only) 025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 026 * as published by the Free Software Foundation. 027 * 028 * This program is distributed in the hope that it will be useful, 029 * but WITHOUT ANY WARRANTY; without even the implied warranty of 030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 031 * GNU General Public License for more details. 032 * 033 * You should have received a copy of the GNU General Public License 034 * along with this program; if not, see <http://www.gnu.org/licenses>. 035 */ 036package com.unboundid.ldap.sdk.schema; 037 038 039 040import java.util.ArrayList; 041import java.util.Collections; 042import java.util.HashSet; 043import java.util.Map; 044import java.util.LinkedHashMap; 045 046import com.unboundid.ldap.sdk.LDAPException; 047import com.unboundid.ldap.sdk.ResultCode; 048import com.unboundid.util.Debug; 049import com.unboundid.util.NotMutable; 050import com.unboundid.util.NotNull; 051import com.unboundid.util.Nullable; 052import com.unboundid.util.StaticUtils; 053import com.unboundid.util.ThreadSafety; 054import com.unboundid.util.ThreadSafetyLevel; 055import com.unboundid.util.Validator; 056 057import static com.unboundid.ldap.sdk.schema.SchemaMessages.*; 058 059 060 061/** 062 * This class provides a data structure that describes an LDAP DIT structure 063 * rule schema element. 064 */ 065@NotMutable() 066@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 067public final class DITStructureRuleDefinition 068 extends SchemaElement 069{ 070 /** 071 * A pre-allocated zero-element integer array. 072 */ 073 @NotNull private static final int[] NO_INTS = new int[0]; 074 075 076 077 /** 078 * The serial version UID for this serializable class. 079 */ 080 private static final long serialVersionUID = -3233223742542121140L; 081 082 083 084 // Indicates whether this DIT structure rule is declared obsolete. 085 private final boolean isObsolete; 086 087 // The rule ID for this DIT structure rule. 088 private final int ruleID; 089 090 // The set of superior rule IDs for this DIT structure rule. 091 @NotNull private final int[] superiorRuleIDs; 092 093 // The set of extensions for this DIT content rule. 094 @NotNull private final Map<String,String[]> extensions; 095 096 // The description for this DIT content rule. 097 @Nullable private final String description; 098 099 // The string representation of this DIT structure rule. 100 @NotNull private final String ditStructureRuleString; 101 102 // The name/OID of the name form with which this DIT structure rule is 103 // associated. 104 @NotNull private final String nameFormID; 105 106 // The set of names for this DIT structure rule. 107 @NotNull private final String[] names; 108 109 110 111 /** 112 * Creates a new DIT structure rule from the provided string representation. 113 * 114 * @param s The string representation of the DIT structure rule to create, 115 * using the syntax described in RFC 4512 section 4.1.7.1. It must 116 * not be {@code null}. 117 * 118 * @throws LDAPException If the provided string cannot be decoded as a DIT 119 * structure rule definition. 120 */ 121 public DITStructureRuleDefinition(@NotNull final String s) 122 throws LDAPException 123 { 124 Validator.ensureNotNull(s); 125 126 ditStructureRuleString = s.trim(); 127 128 // The first character must be an opening parenthesis. 129 final int length = ditStructureRuleString.length(); 130 if (length == 0) 131 { 132 throw new LDAPException(ResultCode.DECODING_ERROR, 133 ERR_DSR_DECODE_EMPTY.get()); 134 } 135 else if (ditStructureRuleString.charAt(0) != '(') 136 { 137 throw new LDAPException(ResultCode.DECODING_ERROR, 138 ERR_DSR_DECODE_NO_OPENING_PAREN.get( 139 ditStructureRuleString)); 140 } 141 142 143 // Skip over any spaces until we reach the start of the OID, then read the 144 // rule ID until we find the next space. 145 int pos = skipSpaces(ditStructureRuleString, 1, length); 146 147 StringBuilder buffer = new StringBuilder(); 148 pos = readOID(ditStructureRuleString, pos, length, buffer); 149 final String ruleIDStr = buffer.toString(); 150 try 151 { 152 ruleID = Integer.parseInt(ruleIDStr); 153 } 154 catch (final NumberFormatException nfe) 155 { 156 Debug.debugException(nfe); 157 throw new LDAPException(ResultCode.DECODING_ERROR, 158 ERR_DSR_DECODE_RULE_ID_NOT_INT.get( 159 ditStructureRuleString), 160 nfe); 161 } 162 163 164 // Technically, DIT structure elements are supposed to appear in a specific 165 // order, but we'll be lenient and allow remaining elements to come in any 166 // order. 167 final ArrayList<Integer> supList = new ArrayList<>(1); 168 final ArrayList<String> nameList = new ArrayList<>(1); 169 final Map<String,String[]> exts = 170 new LinkedHashMap<>(StaticUtils.computeMapCapacity(5)); 171 Boolean obsolete = null; 172 String descr = null; 173 String nfID = null; 174 175 while (true) 176 { 177 // Skip over any spaces until we find the next element. 178 pos = skipSpaces(ditStructureRuleString, pos, length); 179 180 // Read until we find the next space or the end of the string. Use that 181 // token to figure out what to do next. 182 final int tokenStartPos = pos; 183 while ((pos < length) && (ditStructureRuleString.charAt(pos) != ' ')) 184 { 185 pos++; 186 } 187 188 // It's possible that the token could be smashed right up against the 189 // closing parenthesis. If that's the case, then extract just the token 190 // and handle the closing parenthesis the next time through. 191 String token = ditStructureRuleString.substring(tokenStartPos, pos); 192 if ((token.length() > 1) && (token.endsWith(")"))) 193 { 194 token = token.substring(0, token.length() - 1); 195 pos--; 196 } 197 198 final String lowerToken = StaticUtils.toLowerCase(token); 199 if (lowerToken.equals(")")) 200 { 201 // This indicates that we're at the end of the value. There should not 202 // be any more closing characters. 203 if (pos < length) 204 { 205 throw new LDAPException(ResultCode.DECODING_ERROR, 206 ERR_DSR_DECODE_CLOSE_NOT_AT_END.get( 207 ditStructureRuleString)); 208 } 209 break; 210 } 211 else if (lowerToken.equals("name")) 212 { 213 if (nameList.isEmpty()) 214 { 215 pos = skipSpaces(ditStructureRuleString, pos, length); 216 pos = readQDStrings(ditStructureRuleString, pos, length, token, 217 nameList); 218 } 219 else 220 { 221 throw new LDAPException(ResultCode.DECODING_ERROR, 222 ERR_DSR_DECODE_MULTIPLE_ELEMENTS.get( 223 ditStructureRuleString, "NAME")); 224 } 225 } 226 else if (lowerToken.equals("desc")) 227 { 228 if (descr == null) 229 { 230 pos = skipSpaces(ditStructureRuleString, pos, length); 231 232 buffer = new StringBuilder(); 233 pos = readQDString(ditStructureRuleString, pos, length, token, 234 buffer); 235 descr = buffer.toString(); 236 } 237 else 238 { 239 throw new LDAPException(ResultCode.DECODING_ERROR, 240 ERR_DSR_DECODE_MULTIPLE_ELEMENTS.get( 241 ditStructureRuleString, "DESC")); 242 } 243 } 244 else if (lowerToken.equals("obsolete")) 245 { 246 if (obsolete == null) 247 { 248 obsolete = true; 249 } 250 else 251 { 252 throw new LDAPException(ResultCode.DECODING_ERROR, 253 ERR_DSR_DECODE_MULTIPLE_ELEMENTS.get( 254 ditStructureRuleString, "OBSOLETE")); 255 } 256 } 257 else if (lowerToken.equals("form")) 258 { 259 if (nfID == null) 260 { 261 pos = skipSpaces(ditStructureRuleString, pos, length); 262 263 buffer = new StringBuilder(); 264 pos = readOID(ditStructureRuleString, pos, length, buffer); 265 nfID = buffer.toString(); 266 } 267 else 268 { 269 throw new LDAPException(ResultCode.DECODING_ERROR, 270 ERR_DSR_DECODE_MULTIPLE_ELEMENTS.get( 271 ditStructureRuleString, "FORM")); 272 } 273 } 274 else if (lowerToken.equals("sup")) 275 { 276 if (supList.isEmpty()) 277 { 278 final ArrayList<String> supStrs = new ArrayList<>(1); 279 280 pos = skipSpaces(ditStructureRuleString, pos, length); 281 pos = readOIDs(ditStructureRuleString, pos, length, token, supStrs); 282 283 supList.ensureCapacity(supStrs.size()); 284 for (final String supStr : supStrs) 285 { 286 try 287 { 288 supList.add(Integer.parseInt(supStr)); 289 } 290 catch (final NumberFormatException nfe) 291 { 292 Debug.debugException(nfe); 293 throw new LDAPException(ResultCode.DECODING_ERROR, 294 ERR_DSR_DECODE_SUP_ID_NOT_INT.get( 295 ditStructureRuleString), 296 nfe); 297 } 298 } 299 } 300 else 301 { 302 throw new LDAPException(ResultCode.DECODING_ERROR, 303 ERR_DSR_DECODE_MULTIPLE_ELEMENTS.get( 304 ditStructureRuleString, "SUP")); 305 } 306 } 307 else if (lowerToken.startsWith("x-")) 308 { 309 pos = skipSpaces(ditStructureRuleString, pos, length); 310 311 final ArrayList<String> valueList = new ArrayList<>(5); 312 pos = readQDStrings(ditStructureRuleString, pos, length, token, 313 valueList); 314 315 final String[] values = new String[valueList.size()]; 316 valueList.toArray(values); 317 318 if (exts.containsKey(token)) 319 { 320 throw new LDAPException(ResultCode.DECODING_ERROR, 321 ERR_DSR_DECODE_DUP_EXT.get( 322 ditStructureRuleString, token)); 323 } 324 325 exts.put(token, values); 326 } 327 else 328 { 329 throw new LDAPException(ResultCode.DECODING_ERROR, 330 ERR_DSR_DECODE_UNEXPECTED_TOKEN.get( 331 ditStructureRuleString, token)); 332 } 333 } 334 335 description = descr; 336 nameFormID = nfID; 337 338 if (nameFormID == null) 339 { 340 throw new LDAPException(ResultCode.DECODING_ERROR, 341 ERR_DSR_DECODE_NO_FORM.get( 342 ditStructureRuleString)); 343 } 344 345 names = new String[nameList.size()]; 346 nameList.toArray(names); 347 348 superiorRuleIDs = new int[supList.size()]; 349 for (int i=0; i < superiorRuleIDs.length; i++) 350 { 351 superiorRuleIDs[i] = supList.get(i); 352 } 353 354 isObsolete = (obsolete != null); 355 356 extensions = Collections.unmodifiableMap(exts); 357 } 358 359 360 361 /** 362 * Creates a new DIT structure rule with the provided information. 363 * 364 * @param ruleID The rule ID for this DIT structure rule. 365 * @param name The name for this DIT structure rule. It may be 366 * {@code null} if the DIT structure rule should only 367 * be referenced by rule ID. 368 * @param description The description for this DIT structure rule. It 369 * may be {@code null} if there is no description. 370 * @param nameFormID The name or OID of the name form with which this 371 * DIT structure rule is associated. It must not be 372 * {@code null}. 373 * @param superiorRuleID The superior rule ID for this DIT structure rule. 374 * It may be {@code null} if there are no superior 375 * rule IDs. 376 * @param extensions The set of extensions for this DIT structure rule. 377 * It may be {@code null} or empty if there are no 378 * extensions. 379 */ 380 public DITStructureRuleDefinition(final int ruleID, 381 @Nullable final String name, 382 @Nullable final String description, 383 @NotNull final String nameFormID, 384 @Nullable final Integer superiorRuleID, 385 @Nullable final Map<String,String[]> extensions) 386 { 387 this(ruleID, ((name == null) ? null : new String[] { name }), description, 388 false, nameFormID, 389 ((superiorRuleID == null) ? null : new int[] { superiorRuleID }), 390 extensions); 391 } 392 393 394 395 /** 396 * Creates a new DIT structure rule with the provided information. 397 * 398 * @param ruleID The rule ID for this DIT structure rule. 399 * @param names The set of names for this DIT structure rule. It 400 * may be {@code null} or empty if the DIT structure 401 * rule should only be referenced by rule ID. 402 * @param description The description for this DIT structure rule. It 403 * may be {@code null} if there is no description. 404 * @param isObsolete Indicates whether this DIT structure rule is 405 * declared obsolete. 406 * @param nameFormID The name or OID of the name form with which this 407 * DIT structure rule is associated. It must not be 408 * {@code null}. 409 * @param superiorRuleIDs The superior rule IDs for this DIT structure rule. 410 * It may be {@code null} or empty if there are no 411 * superior rule IDs. 412 * @param extensions The set of extensions for this DIT structure rule. 413 * It may be {@code null} or empty if there are no 414 * extensions. 415 */ 416 public DITStructureRuleDefinition(final int ruleID, 417 @Nullable final String[] names, 418 @Nullable final String description, 419 final boolean isObsolete, 420 @NotNull final String nameFormID, 421 @Nullable final int[] superiorRuleIDs, 422 @Nullable final Map<String,String[]> extensions) 423 { 424 Validator.ensureNotNull(nameFormID); 425 426 this.ruleID = ruleID; 427 this.description = description; 428 this.isObsolete = isObsolete; 429 this.nameFormID = nameFormID; 430 431 if (names == null) 432 { 433 this.names = StaticUtils.NO_STRINGS; 434 } 435 else 436 { 437 this.names = names; 438 } 439 440 if (superiorRuleIDs == null) 441 { 442 this.superiorRuleIDs = NO_INTS; 443 } 444 else 445 { 446 this.superiorRuleIDs = superiorRuleIDs; 447 } 448 449 if (extensions == null) 450 { 451 this.extensions = Collections.emptyMap(); 452 } 453 else 454 { 455 this.extensions = Collections.unmodifiableMap(extensions); 456 } 457 458 final StringBuilder buffer = new StringBuilder(); 459 createDefinitionString(buffer); 460 ditStructureRuleString = buffer.toString(); 461 } 462 463 464 465 /** 466 * Constructs a string representation of this DIT content rule definition in 467 * the provided buffer. 468 * 469 * @param buffer The buffer in which to construct a string representation of 470 * this DIT content rule definition. 471 */ 472 private void createDefinitionString(@NotNull final StringBuilder buffer) 473 { 474 buffer.append("( "); 475 buffer.append(ruleID); 476 477 if (names.length == 1) 478 { 479 buffer.append(" NAME '"); 480 buffer.append(names[0]); 481 buffer.append('\''); 482 } 483 else if (names.length > 1) 484 { 485 buffer.append(" NAME ("); 486 for (final String name : names) 487 { 488 buffer.append(" '"); 489 buffer.append(name); 490 buffer.append('\''); 491 } 492 buffer.append(" )"); 493 } 494 495 if (description != null) 496 { 497 buffer.append(" DESC '"); 498 encodeValue(description, buffer); 499 buffer.append('\''); 500 } 501 502 if (isObsolete) 503 { 504 buffer.append(" OBSOLETE"); 505 } 506 507 buffer.append(" FORM "); 508 buffer.append(nameFormID); 509 510 if (superiorRuleIDs.length == 1) 511 { 512 buffer.append(" SUP "); 513 buffer.append(superiorRuleIDs[0]); 514 } 515 else if (superiorRuleIDs.length > 1) 516 { 517 buffer.append(" SUP ("); 518 for (final int supID : superiorRuleIDs) 519 { 520 buffer.append(" $ "); 521 buffer.append(supID); 522 } 523 buffer.append(" )"); 524 } 525 526 for (final Map.Entry<String,String[]> e : extensions.entrySet()) 527 { 528 final String name = e.getKey(); 529 final String[] values = e.getValue(); 530 if (values.length == 1) 531 { 532 buffer.append(' '); 533 buffer.append(name); 534 buffer.append(" '"); 535 encodeValue(values[0], buffer); 536 buffer.append('\''); 537 } 538 else 539 { 540 buffer.append(' '); 541 buffer.append(name); 542 buffer.append(" ("); 543 for (final String value : values) 544 { 545 buffer.append(" '"); 546 encodeValue(value, buffer); 547 buffer.append('\''); 548 } 549 buffer.append(" )"); 550 } 551 } 552 553 buffer.append(" )"); 554 } 555 556 557 558 /** 559 * Retrieves the rule ID for this DIT structure rule. 560 * 561 * @return The rule ID for this DIT structure rule. 562 */ 563 public int getRuleID() 564 { 565 return ruleID; 566 } 567 568 569 570 /** 571 * Retrieves the set of names for this DIT structure rule. 572 * 573 * @return The set of names for this DIT structure rule, or an empty array if 574 * it does not have any names. 575 */ 576 @NotNull() 577 public String[] getNames() 578 { 579 return names; 580 } 581 582 583 584 /** 585 * Retrieves the primary name that can be used to reference this DIT structure 586 * rule. If one or more names are defined, then the first name will be used. 587 * Otherwise, the string representation of the rule ID will be returned. 588 * 589 * @return The primary name that can be used to reference this DIT structure 590 * rule. 591 */ 592 @NotNull() 593 public String getNameOrRuleID() 594 { 595 if (names.length == 0) 596 { 597 return String.valueOf(ruleID); 598 } 599 else 600 { 601 return names[0]; 602 } 603 } 604 605 606 607 /** 608 * Indicates whether the provided string matches the rule ID or any of the 609 * names for this DIT structure rule. 610 * 611 * @param s The string for which to make the determination. It must not be 612 * {@code null}. 613 * 614 * @return {@code true} if the provided string matches the rule ID or any of 615 * the names for this DIT structure rule, or {@code false} if not. 616 */ 617 public boolean hasNameOrRuleID(@NotNull final String s) 618 { 619 for (final String name : names) 620 { 621 if (s.equalsIgnoreCase(name)) 622 { 623 return true; 624 } 625 } 626 627 return s.equalsIgnoreCase(String.valueOf(ruleID)); 628 } 629 630 631 632 /** 633 * Retrieves the description for this DIT structure rule, if available. 634 * 635 * @return The description for this DIT structure rule, or {@code null} if 636 * there is no description defined. 637 */ 638 @Nullable() 639 public String getDescription() 640 { 641 return description; 642 } 643 644 645 646 /** 647 * Indicates whether this DIT structure rule is declared obsolete. 648 * 649 * @return {@code true} if this DIT structure rule is declared obsolete, or 650 * {@code false} if it is not. 651 */ 652 public boolean isObsolete() 653 { 654 return isObsolete; 655 } 656 657 658 659 /** 660 * Retrieves the name or OID of the name form with which this DIT structure 661 * rule is associated. 662 * 663 * @return The name or OID of the name form with which this DIT structure 664 * rule is associated. 665 */ 666 @NotNull() 667 public String getNameFormID() 668 { 669 return nameFormID; 670 } 671 672 673 674 /** 675 * Retrieves the rule IDs of the superior rules for this DIT structure rule. 676 * 677 * @return The rule IDs of the superior rules for this DIT structure rule, or 678 * an empty array if there are no superior rule IDs. 679 */ 680 @NotNull() 681 public int[] getSuperiorRuleIDs() 682 { 683 return superiorRuleIDs; 684 } 685 686 687 688 /** 689 * Retrieves the set of extensions for this DIT structure rule. They will be 690 * mapped from the extension name (which should start with "X-") to the set of 691 * values for that extension. 692 * 693 * @return The set of extensions for this DIT structure rule. 694 */ 695 @NotNull() 696 public Map<String,String[]> getExtensions() 697 { 698 return extensions; 699 } 700 701 702 703 /** 704 * {@inheritDoc} 705 */ 706 @Override() 707 @NotNull() 708 public SchemaElementType getSchemaElementType() 709 { 710 return SchemaElementType.DIT_STRUCTURE_RULE; 711 } 712 713 714 715 /** 716 * {@inheritDoc} 717 */ 718 @Override() 719 public int hashCode() 720 { 721 return ruleID; 722 } 723 724 725 726 /** 727 * {@inheritDoc} 728 */ 729 @Override() 730 public boolean equals(@Nullable final Object o) 731 { 732 if (o == null) 733 { 734 return false; 735 } 736 737 if (o == this) 738 { 739 return true; 740 } 741 742 if (! (o instanceof DITStructureRuleDefinition)) 743 { 744 return false; 745 } 746 747 final DITStructureRuleDefinition d = (DITStructureRuleDefinition) o; 748 if ((ruleID == d.ruleID) && 749 nameFormID.equalsIgnoreCase(d.nameFormID) && 750 StaticUtils.stringsEqualIgnoreCaseOrderIndependent(names, d.names) && 751 (isObsolete == d.isObsolete) && 752 extensionsEqual(extensions, d.extensions)) 753 { 754 if (superiorRuleIDs.length != d.superiorRuleIDs.length) 755 { 756 return false; 757 } 758 759 final HashSet<Integer> s1 = new HashSet<>( 760 StaticUtils.computeMapCapacity(superiorRuleIDs.length)); 761 final HashSet<Integer> s2 = new HashSet<>( 762 StaticUtils.computeMapCapacity(superiorRuleIDs.length)); 763 for (final int i : superiorRuleIDs) 764 { 765 s1.add(i); 766 } 767 768 for (final int i : d.superiorRuleIDs) 769 { 770 s2.add(i); 771 } 772 773 return s1.equals(s2); 774 } 775 else 776 { 777 return false; 778 } 779 } 780 781 782 783 /** 784 * Retrieves a string representation of this DIT structure rule definition, in 785 * the format described in RFC 4512 section 4.1.7.1. 786 * 787 * @return A string representation of this DIT structure rule definition. 788 */ 789 @Override() 790 @NotNull() 791 public String toString() 792 { 793 return ditStructureRuleString; 794 } 795}