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