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