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.io.Serializable; 041import java.nio.ByteBuffer; 042import java.util.ArrayList; 043import java.util.Collection; 044import java.util.Map; 045 046import com.unboundid.ldap.sdk.LDAPException; 047import com.unboundid.ldap.sdk.ResultCode; 048import com.unboundid.util.NotExtensible; 049import com.unboundid.util.NotNull; 050import com.unboundid.util.Nullable; 051import com.unboundid.util.StaticUtils; 052import com.unboundid.util.ThreadSafety; 053import com.unboundid.util.ThreadSafetyLevel; 054 055import static com.unboundid.ldap.sdk.schema.SchemaMessages.*; 056 057 058 059/** 060 * This class provides a superclass for all schema element types, and defines a 061 * number of utility methods that may be used when parsing schema element 062 * strings. 063 */ 064@NotExtensible() 065@ThreadSafety(level=ThreadSafetyLevel.INTERFACE_THREADSAFE) 066public abstract class SchemaElement 067 implements Serializable 068{ 069 /** 070 * Indicates whether schema elements will be permitted to use an empty 071 * quoted string as the value of the {@code DESC} component. 072 */ 073 private static boolean allowEmptyDescription = Boolean.getBoolean( 074 "com.unboundid.ldap.sdk.schema.AllowEmptyDescription"); 075 076 077 078 /** 079 * The serial version UID for this serializable class. 080 */ 081 private static final long serialVersionUID = -8249972237068748580L; 082 083 084 085 /** 086 * Indicates whether to allow schema elements to contain an empty string as 087 * the value for the {@code DESC} component. Although quoted strings are not 088 * allowed in schema elements as per RFC 4512 section 4.1, some directory 089 * servers allow it, and it may be necessary to support schema definitions 090 * used in conjunction with those servers. 091 * <BR><BR> 092 * The LDAP SDK does not allow empty schema element descriptions by default, 093 * but it may be updated to allow it using either the 094 * {@link #setAllowEmptyDescription} method or by setting the value of the 095 * {@code com.unboundid.ldap.sdk.schema.AllowEmptyDescription} system property 096 * to {@code true} before this class is loaded. 097 * 098 * @return {@code true} if the LDAP SDK should allow schema elements with 099 * empty descriptions, or {@code false} if not. 100 */ 101 public static boolean allowEmptyDescription() 102 { 103 return allowEmptyDescription; 104 } 105 106 107 108 /** 109 * Specifies whether to allow schema elements to contain an empty string as 110 * the value for the {@code DESC} component. If specified, this will override 111 * the value of the 112 * {@code com.unboundid.ldap.sdk.schema.AllowEmptyDescription} system 113 * property. 114 * 115 * @param allowEmptyDescription Indicates whether to allow schema elements 116 * to contain an empty string as the value for 117 * the {@code DESC} component. 118 */ 119 public static void setAllowEmptyDescription( 120 final boolean allowEmptyDescription) 121 { 122 SchemaElement.allowEmptyDescription = allowEmptyDescription; 123 } 124 125 126 127 /** 128 * Skips over any any spaces in the provided string. 129 * 130 * @param s The string in which to skip the spaces. 131 * @param startPos The position at which to start skipping spaces. 132 * @param length The position of the end of the string. 133 * 134 * @return The position of the next non-space character in the string. 135 * 136 * @throws LDAPException If the end of the string was reached without 137 * finding a non-space character. 138 */ 139 static int skipSpaces(@NotNull final String s, final int startPos, 140 final int length) 141 throws LDAPException 142 { 143 int pos = startPos; 144 while ((pos < length) && (s.charAt(pos) == ' ')) 145 { 146 pos++; 147 } 148 149 if (pos >= length) 150 { 151 throw new LDAPException(ResultCode.DECODING_ERROR, 152 ERR_SCHEMA_ELEM_SKIP_SPACES_NO_CLOSE_PAREN.get(s)); 153 } 154 155 return pos; 156 } 157 158 159 160 /** 161 * Reads one or more hex-encoded bytes from the specified portion of the RDN 162 * string. 163 * 164 * @param s The string from which the data is to be read. 165 * @param startPos The position at which to start reading. This should 166 * be the first hex character immediately after the 167 * initial backslash. 168 * @param length The position of the end of the string. 169 * @param componentName The name of the component in the schema element 170 * definition whose value is being read. 171 * @param buffer The buffer to which the decoded string portion 172 * should be appended. 173 * 174 * @return The position at which the caller may resume parsing. 175 * 176 * @throws LDAPException If a problem occurs while reading hex-encoded 177 * bytes. 178 */ 179 private static int readEscapedHexString(@NotNull final String s, 180 final int startPos, 181 final int length, 182 @NotNull final String componentName, 183 @NotNull final StringBuilder buffer) 184 throws LDAPException 185 { 186 int pos = startPos; 187 188 final ByteBuffer byteBuffer = ByteBuffer.allocate(length - pos); 189 while (pos < length) 190 { 191 final byte b; 192 switch (s.charAt(pos++)) 193 { 194 case '0': 195 b = 0x00; 196 break; 197 case '1': 198 b = 0x10; 199 break; 200 case '2': 201 b = 0x20; 202 break; 203 case '3': 204 b = 0x30; 205 break; 206 case '4': 207 b = 0x40; 208 break; 209 case '5': 210 b = 0x50; 211 break; 212 case '6': 213 b = 0x60; 214 break; 215 case '7': 216 b = 0x70; 217 break; 218 case '8': 219 b = (byte) 0x80; 220 break; 221 case '9': 222 b = (byte) 0x90; 223 break; 224 case 'a': 225 case 'A': 226 b = (byte) 0xA0; 227 break; 228 case 'b': 229 case 'B': 230 b = (byte) 0xB0; 231 break; 232 case 'c': 233 case 'C': 234 b = (byte) 0xC0; 235 break; 236 case 'd': 237 case 'D': 238 b = (byte) 0xD0; 239 break; 240 case 'e': 241 case 'E': 242 b = (byte) 0xE0; 243 break; 244 case 'f': 245 case 'F': 246 b = (byte) 0xF0; 247 break; 248 default: 249 throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 250 ERR_SCHEMA_ELEM_INVALID_HEX_CHAR.get(s, s.charAt(pos-1), 251 (pos-1), componentName)); 252 } 253 254 if (pos >= length) 255 { 256 throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 257 ERR_SCHEMA_ELEM_MISSING_HEX_CHAR.get(s, componentName)); 258 } 259 260 switch (s.charAt(pos++)) 261 { 262 case '0': 263 byteBuffer.put(b); 264 break; 265 case '1': 266 byteBuffer.put((byte) (b | 0x01)); 267 break; 268 case '2': 269 byteBuffer.put((byte) (b | 0x02)); 270 break; 271 case '3': 272 byteBuffer.put((byte) (b | 0x03)); 273 break; 274 case '4': 275 byteBuffer.put((byte) (b | 0x04)); 276 break; 277 case '5': 278 byteBuffer.put((byte) (b | 0x05)); 279 break; 280 case '6': 281 byteBuffer.put((byte) (b | 0x06)); 282 break; 283 case '7': 284 byteBuffer.put((byte) (b | 0x07)); 285 break; 286 case '8': 287 byteBuffer.put((byte) (b | 0x08)); 288 break; 289 case '9': 290 byteBuffer.put((byte) (b | 0x09)); 291 break; 292 case 'a': 293 case 'A': 294 byteBuffer.put((byte) (b | 0x0A)); 295 break; 296 case 'b': 297 case 'B': 298 byteBuffer.put((byte) (b | 0x0B)); 299 break; 300 case 'c': 301 case 'C': 302 byteBuffer.put((byte) (b | 0x0C)); 303 break; 304 case 'd': 305 case 'D': 306 byteBuffer.put((byte) (b | 0x0D)); 307 break; 308 case 'e': 309 case 'E': 310 byteBuffer.put((byte) (b | 0x0E)); 311 break; 312 case 'f': 313 case 'F': 314 byteBuffer.put((byte) (b | 0x0F)); 315 break; 316 default: 317 throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 318 ERR_SCHEMA_ELEM_INVALID_HEX_CHAR.get(s, s.charAt(pos-1), 319 (pos-1), componentName)); 320 } 321 322 if (((pos+1) < length) && (s.charAt(pos) == '\\') && 323 StaticUtils.isHex(s.charAt(pos+1))) 324 { 325 // It appears that there are more hex-encoded bytes to follow, so keep 326 // reading. 327 pos++; 328 continue; 329 } 330 else 331 { 332 break; 333 } 334 } 335 336 byteBuffer.flip(); 337 final byte[] byteArray = new byte[byteBuffer.limit()]; 338 byteBuffer.get(byteArray); 339 buffer.append(StaticUtils.toUTF8String(byteArray)); 340 return pos; 341 } 342 343 344 345 /** 346 * Reads a single-quoted string from the provided string. 347 * 348 * @param s The string from which to read the single-quoted 349 * string. 350 * @param startPos The position at which to start reading. 351 * @param length The position of the end of the string. 352 * @param componentName The name of the component in the schema element 353 * definition whose value is being read. 354 * @param buffer The buffer into which the single-quoted string 355 * should be placed (without the surrounding single 356 * quotes). 357 * 358 * @return The position of the first space immediately following the closing 359 * quote. 360 * 361 * @throws LDAPException If a problem is encountered while attempting to 362 * read the single-quoted string. 363 */ 364 static int readQDString(@NotNull final String s, final int startPos, 365 final int length, @NotNull final String componentName, 366 @NotNull final StringBuilder buffer) 367 throws LDAPException 368 { 369 // The first character must be a single quote. 370 if (s.charAt(startPos) != '\'') 371 { 372 throw new LDAPException(ResultCode.DECODING_ERROR, 373 ERR_SCHEMA_ELEM_EXPECTED_SINGLE_QUOTE.get(s, startPos, 374 componentName)); 375 } 376 377 // Read until we find the next closing quote. If we find any hex-escaped 378 // characters along the way, then decode them. 379 int pos = startPos + 1; 380 while (pos < length) 381 { 382 final char c = s.charAt(pos++); 383 if (c == '\'') 384 { 385 // This is the end of the quoted string. 386 break; 387 } 388 else if (c == '\\') 389 { 390 // This designates the beginning of one or more hex-encoded bytes. 391 if (pos >= length) 392 { 393 throw new LDAPException(ResultCode.DECODING_ERROR, 394 ERR_SCHEMA_ELEM_ENDS_WITH_BACKSLASH.get(s, componentName)); 395 } 396 397 pos = readEscapedHexString(s, pos, length, componentName, buffer); 398 } 399 else 400 { 401 buffer.append(c); 402 } 403 } 404 405 if ((pos >= length) || ((s.charAt(pos) != ' ') && (s.charAt(pos) != ')'))) 406 { 407 throw new LDAPException(ResultCode.DECODING_ERROR, 408 ERR_SCHEMA_ELEM_NO_CLOSING_PAREN.get(s, componentName)); 409 } 410 411 if (buffer.length() == 0) 412 { 413 if (! (allowEmptyDescription && componentName.equalsIgnoreCase("DESC"))) 414 { 415 throw new LDAPException(ResultCode.DECODING_ERROR, 416 ERR_SCHEMA_ELEM_EMPTY_QUOTES.get(s, componentName)); 417 } 418 } 419 420 return pos; 421 } 422 423 424 425 /** 426 * Reads one a set of one or more single-quoted strings from the provided 427 * string. The value to read may be either a single string enclosed in 428 * single quotes, or an opening parenthesis followed by a space followed by 429 * one or more space-delimited single-quoted strings, followed by a space and 430 * a closing parenthesis. 431 * 432 * @param s The string from which to read the single-quoted 433 * strings. 434 * @param startPos The position at which to start reading. 435 * @param length The position of the end of the string. 436 * @param componentName The name of the component in the schema element 437 * definition whose value is being read. 438 * @param valueList The list into which the values read may be placed. 439 * 440 * @return The position of the first space immediately following the end of 441 * the values. 442 * 443 * @throws LDAPException If a problem is encountered while attempting to 444 * read the single-quoted strings. 445 */ 446 static int readQDStrings(@NotNull final String s, final int startPos, 447 final int length, 448 @NotNull final String componentName, 449 @NotNull final ArrayList<String> valueList) 450 throws LDAPException 451 { 452 // Look at the first character. It must be either a single quote or an 453 // opening parenthesis. 454 char c = s.charAt(startPos); 455 if (c == '\'') 456 { 457 // It's just a single value, so use the readQDString method to get it. 458 final StringBuilder buffer = new StringBuilder(); 459 final int returnPos = readQDString(s, startPos, length, componentName, 460 buffer); 461 valueList.add(buffer.toString()); 462 return returnPos; 463 } 464 else if (c == '(') 465 { 466 int pos = startPos + 1; 467 while (true) 468 { 469 pos = skipSpaces(s, pos, length); 470 c = s.charAt(pos); 471 if (c == ')') 472 { 473 // This is the end of the value list. 474 pos++; 475 break; 476 } 477 else if (c == '\'') 478 { 479 // This is the next value in the list. 480 final StringBuilder buffer = new StringBuilder(); 481 pos = readQDString(s, pos, length, componentName, buffer); 482 valueList.add(buffer.toString()); 483 } 484 else 485 { 486 throw new LDAPException(ResultCode.DECODING_ERROR, 487 ERR_SCHEMA_ELEM_EXPECTED_QUOTE_OR_PAREN.get(s, startPos, 488 componentName)); 489 } 490 } 491 492 if (valueList.isEmpty()) 493 { 494 throw new LDAPException(ResultCode.DECODING_ERROR, 495 ERR_SCHEMA_ELEM_EMPTY_STRING_LIST.get(s, componentName)); 496 } 497 498 if ((pos >= length) || 499 ((s.charAt(pos) != ' ') && (s.charAt(pos) != ')'))) 500 { 501 throw new LDAPException(ResultCode.DECODING_ERROR, 502 ERR_SCHEMA_ELEM_NO_SPACE_AFTER_QUOTE.get(s, componentName)); 503 } 504 505 return pos; 506 } 507 else 508 { 509 throw new LDAPException(ResultCode.DECODING_ERROR, 510 ERR_SCHEMA_ELEM_EXPECTED_QUOTE_OR_PAREN.get(s, startPos, 511 componentName)); 512 } 513 } 514 515 516 517 /** 518 * Reads an OID value from the provided string. The OID value may be either a 519 * numeric OID or a string name. This implementation will be fairly lenient 520 * with regard to the set of characters that may be present, and it will 521 * allow the OID to be enclosed in single quotes. 522 * 523 * @param s The string from which to read the OID string. 524 * @param startPos The position at which to start reading. 525 * @param length The position of the end of the string. 526 * @param buffer The buffer into which the OID string should be placed. 527 * 528 * @return The position of the first space immediately following the OID 529 * string. 530 * 531 * @throws LDAPException If a problem is encountered while attempting to 532 * read the OID string. 533 */ 534 static int readOID(@NotNull final String s, final int startPos, 535 final int length, @NotNull final StringBuilder buffer) 536 throws LDAPException 537 { 538 // Read until we find the first space. 539 int pos = startPos; 540 boolean lastWasQuote = false; 541 while (pos < length) 542 { 543 final char c = s.charAt(pos); 544 if ((c == ' ') || (c == '$') || (c == ')')) 545 { 546 if (buffer.length() == 0) 547 { 548 throw new LDAPException(ResultCode.DECODING_ERROR, 549 ERR_SCHEMA_ELEM_EMPTY_OID.get(s)); 550 } 551 552 return pos; 553 } 554 else if (((c >= 'a') && (c <= 'z')) || 555 ((c >= 'A') && (c <= 'Z')) || 556 ((c >= '0') && (c <= '9')) || 557 (c == '-') || (c == '.') || (c == '_') || 558 (c == '{') || (c == '}')) 559 { 560 if (lastWasQuote) 561 { 562 throw new LDAPException(ResultCode.DECODING_ERROR, 563 ERR_SCHEMA_ELEM_UNEXPECTED_CHAR_IN_OID.get(s, (pos-1))); 564 } 565 566 buffer.append(c); 567 } 568 else if (c == '\'') 569 { 570 if (buffer.length() != 0) 571 { 572 lastWasQuote = true; 573 } 574 } 575 else 576 { 577 throw new LDAPException(ResultCode.DECODING_ERROR, 578 ERR_SCHEMA_ELEM_UNEXPECTED_CHAR_IN_OID.get(s, pos)); 579 } 580 581 pos++; 582 } 583 584 585 // We hit the end of the string before finding a space. 586 throw new LDAPException(ResultCode.DECODING_ERROR, 587 ERR_SCHEMA_ELEM_NO_SPACE_AFTER_OID.get(s)); 588 } 589 590 591 592 /** 593 * Reads one a set of one or more OID strings from the provided string. The 594 * value to read may be either a single OID string or an opening parenthesis 595 * followed by a space followed by one or more space-delimited OID strings, 596 * followed by a space and a closing parenthesis. 597 * 598 * @param s The string from which to read the OID strings. 599 * @param startPos The position at which to start reading. 600 * @param length The position of the end of the string. 601 * @param componentName The name of the component in the schema element 602 * definition whose value is being read. 603 * @param valueList The list into which the values read may be placed. 604 * 605 * @return The position of the first space immediately following the end of 606 * the values. 607 * 608 * @throws LDAPException If a problem is encountered while attempting to 609 * read the OID strings. 610 */ 611 static int readOIDs(@NotNull final String s, final int startPos, 612 final int length, @NotNull final String componentName, 613 @NotNull final ArrayList<String> valueList) 614 throws LDAPException 615 { 616 // Look at the first character. If it's an opening parenthesis, then read 617 // a list of OID strings. Otherwise, just read a single string. 618 char c = s.charAt(startPos); 619 if (c == '(') 620 { 621 int pos = startPos + 1; 622 while (true) 623 { 624 pos = skipSpaces(s, pos, length); 625 c = s.charAt(pos); 626 if (c == ')') 627 { 628 // This is the end of the value list. 629 pos++; 630 break; 631 } 632 else if (c == '$') 633 { 634 // This is the delimiter before the next value in the list. 635 pos++; 636 pos = skipSpaces(s, pos, length); 637 final StringBuilder buffer = new StringBuilder(); 638 pos = readOID(s, pos, length, buffer); 639 valueList.add(buffer.toString()); 640 } 641 else if (valueList.isEmpty()) 642 { 643 // This is the first value in the list. 644 final StringBuilder buffer = new StringBuilder(); 645 pos = readOID(s, pos, length, buffer); 646 valueList.add(buffer.toString()); 647 } 648 else 649 { 650 throw new LDAPException(ResultCode.DECODING_ERROR, 651 ERR_SCHEMA_ELEM_UNEXPECTED_CHAR_IN_OID_LIST.get(s, pos, 652 componentName)); 653 } 654 } 655 656 if (valueList.isEmpty()) 657 { 658 throw new LDAPException(ResultCode.DECODING_ERROR, 659 ERR_SCHEMA_ELEM_EMPTY_OID_LIST.get(s, componentName)); 660 } 661 662 if (pos >= length) 663 { 664 // Technically, there should be a space after the closing parenthesis, 665 // but there are known cases in which servers (like Active Directory) 666 // omit this space, so we'll be lenient and allow a missing space. But 667 // it can't possibly be the end of the schema element definition, so 668 // that's still an error. 669 throw new LDAPException(ResultCode.DECODING_ERROR, 670 ERR_SCHEMA_ELEM_NO_SPACE_AFTER_OID_LIST.get(s, componentName)); 671 } 672 673 return pos; 674 } 675 else 676 { 677 final StringBuilder buffer = new StringBuilder(); 678 final int returnPos = readOID(s, startPos, length, buffer); 679 valueList.add(buffer.toString()); 680 return returnPos; 681 } 682 } 683 684 685 686 /** 687 * Appends a properly-encoded representation of the provided value to the 688 * given buffer. 689 * 690 * @param value The value to be encoded and placed in the buffer. 691 * @param buffer The buffer to which the encoded value is to be appended. 692 */ 693 static void encodeValue(@NotNull final String value, 694 @NotNull final StringBuilder buffer) 695 { 696 final int length = value.length(); 697 for (int i=0; i < length; i++) 698 { 699 final char c = value.charAt(i); 700 if ((c < ' ') || (c > '~') || (c == '\\') || (c == '\'')) 701 { 702 StaticUtils.hexEncode(c, buffer); 703 } 704 else 705 { 706 buffer.append(c); 707 } 708 } 709 } 710 711 712 713 /** 714 * Retrieves the type of schema element that this object represents. 715 * 716 * @return The type of schema element that this object represents. 717 */ 718 @NotNull() 719 public abstract SchemaElementType getSchemaElementType(); 720 721 722 723 /** 724 * Retrieves a hash code for this schema element. 725 * 726 * @return A hash code for this schema element. 727 */ 728 public abstract int hashCode(); 729 730 731 732 /** 733 * Indicates whether the provided object is equal to this schema element. 734 * 735 * @param o The object for which to make the determination. 736 * 737 * @return {@code true} if the provided object may be considered equal to 738 * this schema element, or {@code false} if not. 739 */ 740 public abstract boolean equals(@Nullable Object o); 741 742 743 744 /** 745 * Indicates whether the two extension maps are equivalent. 746 * 747 * @param m1 The first schema element to examine. 748 * @param m2 The second schema element to examine. 749 * 750 * @return {@code true} if the provided extension maps are equivalent, or 751 * {@code false} if not. 752 */ 753 protected static boolean extensionsEqual( 754 @NotNull final Map<String,String[]> m1, 755 @NotNull final Map<String,String[]> m2) 756 { 757 if (m1.isEmpty()) 758 { 759 return m2.isEmpty(); 760 } 761 762 if (m1.size() != m2.size()) 763 { 764 return false; 765 } 766 767 for (final Map.Entry<String,String[]> e : m1.entrySet()) 768 { 769 final String[] v1 = e.getValue(); 770 final String[] v2 = m2.get(e.getKey()); 771 if (! StaticUtils.arraysEqualOrderIndependent(v1, v2)) 772 { 773 return false; 774 } 775 } 776 777 return true; 778 } 779 780 781 782 /** 783 * Converts the provided collection of strings to an array. 784 * 785 * @param c The collection to convert to an array. It may be {@code null}. 786 * 787 * @return A string array if the provided collection is non-{@code null}, or 788 * {@code null} if the provided collection is {@code null}. 789 */ 790 @Nullable() 791 static String[] toArray(@Nullable final Collection<String> c) 792 { 793 if (c == null) 794 { 795 return null; 796 } 797 798 return c.toArray(StaticUtils.NO_STRINGS); 799 } 800 801 802 803 /** 804 * Retrieves a string representation of this schema element, in the format 805 * described in RFC 4512. 806 * 807 * @return A string representation of this schema element, in the format 808 * described in RFC 4512. 809 */ 810 @Override() 811 @NotNull() 812 public abstract String toString(); 813}