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