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