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