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