001/* 002 * Copyright 2007-2017 UnboundID Corp. 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2008-2017 UnboundID Corp. 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 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 // No action is required. 193 break; 194 case '1': 195 b |= 0x01; 196 break; 197 case '2': 198 b |= 0x02; 199 break; 200 case '3': 201 b |= 0x03; 202 break; 203 case '4': 204 b |= 0x04; 205 break; 206 case '5': 207 b |= 0x05; 208 break; 209 case '6': 210 b |= 0x06; 211 break; 212 case '7': 213 b |= 0x07; 214 break; 215 case '8': 216 b |= 0x08; 217 break; 218 case '9': 219 b |= 0x09; 220 break; 221 case 'a': 222 case 'A': 223 b |= 0x0A; 224 break; 225 case 'b': 226 case 'B': 227 b |= 0x0B; 228 break; 229 case 'c': 230 case 'C': 231 b |= 0x0C; 232 break; 233 case 'd': 234 case 'D': 235 b |= 0x0D; 236 break; 237 case 'e': 238 case 'E': 239 b |= 0x0E; 240 break; 241 case 'f': 242 case 'F': 243 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 byteBuffer.put(b); 252 if (((pos+1) < length) && (s.charAt(pos) == '\\') && 253 isHex(s.charAt(pos+1))) 254 { 255 // It appears that there are more hex-encoded bytes to follow, so keep 256 // reading. 257 pos++; 258 continue; 259 } 260 else 261 { 262 break; 263 } 264 } 265 266 byteBuffer.flip(); 267 final byte[] byteArray = new byte[byteBuffer.limit()]; 268 byteBuffer.get(byteArray); 269 270 try 271 { 272 buffer.append(toUTF8String(byteArray)); 273 } 274 catch (final Exception e) 275 { 276 debugException(e); 277 // This should never happen. 278 buffer.append(new String(byteArray)); 279 } 280 281 return pos; 282 } 283 284 285 286 /** 287 * Reads a single-quoted string from the provided string. 288 * 289 * @param s The string from which to read the single-quoted string. 290 * @param startPos The position at which to start reading. 291 * @param length The position of the end of the string. 292 * @param buffer The buffer into which the single-quoted string should be 293 * placed (without the surrounding single quotes). 294 * 295 * @return The position of the first space immediately following the closing 296 * quote. 297 * 298 * @throws LDAPException If a problem is encountered while attempting to 299 * read the single-quoted string. 300 */ 301 static int readQDString(final String s, final int startPos, final int length, 302 final StringBuilder buffer) 303 throws LDAPException 304 { 305 // The first character must be a single quote. 306 if (s.charAt(startPos) != '\'') 307 { 308 throw new LDAPException(ResultCode.DECODING_ERROR, 309 ERR_SCHEMA_ELEM_EXPECTED_SINGLE_QUOTE.get(s, 310 startPos)); 311 } 312 313 // Read until we find the next closing quote. If we find any hex-escaped 314 // characters along the way, then decode them. 315 int pos = startPos + 1; 316 while (pos < length) 317 { 318 final char c = s.charAt(pos++); 319 if (c == '\'') 320 { 321 // This is the end of the quoted string. 322 break; 323 } 324 else if (c == '\\') 325 { 326 // This designates the beginning of one or more hex-encoded bytes. 327 if (pos >= length) 328 { 329 throw new LDAPException(ResultCode.DECODING_ERROR, 330 ERR_SCHEMA_ELEM_ENDS_WITH_BACKSLASH.get(s)); 331 } 332 333 pos = readEscapedHexString(s, pos, length, buffer); 334 } 335 else 336 { 337 buffer.append(c); 338 } 339 } 340 341 if ((pos >= length) || ((s.charAt(pos) != ' ') && (s.charAt(pos) != ')'))) 342 { 343 throw new LDAPException(ResultCode.DECODING_ERROR, 344 ERR_SCHEMA_ELEM_NO_CLOSING_PAREN.get(s)); 345 } 346 347 if (buffer.length() == 0) 348 { 349 throw new LDAPException(ResultCode.DECODING_ERROR, 350 ERR_SCHEMA_ELEM_EMPTY_QUOTES.get(s)); 351 } 352 353 return pos; 354 } 355 356 357 358 /** 359 * Reads one a set of one or more single-quoted strings from the provided 360 * string. The value to read may be either a single string enclosed in 361 * single quotes, or an opening parenthesis followed by a space followed by 362 * one or more space-delimited single-quoted strings, followed by a space and 363 * a closing parenthesis. 364 * 365 * @param s The string from which to read the single-quoted strings. 366 * @param startPos The position at which to start reading. 367 * @param length The position of the end of the string. 368 * @param valueList The list into which the values read may be placed. 369 * 370 * @return The position of the first space immediately following the end of 371 * the values. 372 * 373 * @throws LDAPException If a problem is encountered while attempting to 374 * read the single-quoted strings. 375 */ 376 static int readQDStrings(final String s, final int startPos, final int length, 377 final ArrayList<String> valueList) 378 throws LDAPException 379 { 380 // Look at the first character. It must be either a single quote or an 381 // opening parenthesis. 382 char c = s.charAt(startPos); 383 if (c == '\'') 384 { 385 // It's just a single value, so use the readQDString method to get it. 386 final StringBuilder buffer = new StringBuilder(); 387 final int returnPos = readQDString(s, startPos, length, buffer); 388 valueList.add(buffer.toString()); 389 return returnPos; 390 } 391 else if (c == '(') 392 { 393 int pos = startPos + 1; 394 while (true) 395 { 396 pos = skipSpaces(s, pos, length); 397 c = s.charAt(pos); 398 if (c == ')') 399 { 400 // This is the end of the value list. 401 pos++; 402 break; 403 } 404 else if (c == '\'') 405 { 406 // This is the next value in the list. 407 final StringBuilder buffer = new StringBuilder(); 408 pos = readQDString(s, pos, length, buffer); 409 valueList.add(buffer.toString()); 410 } 411 else 412 { 413 throw new LDAPException(ResultCode.DECODING_ERROR, 414 ERR_SCHEMA_ELEM_EXPECTED_QUOTE_OR_PAREN.get( 415 s, startPos)); 416 } 417 } 418 419 if (valueList.isEmpty()) 420 { 421 throw new LDAPException(ResultCode.DECODING_ERROR, 422 ERR_SCHEMA_ELEM_EMPTY_STRING_LIST.get(s)); 423 } 424 425 if ((pos >= length) || 426 ((s.charAt(pos) != ' ') && (s.charAt(pos) != ')'))) 427 { 428 throw new LDAPException(ResultCode.DECODING_ERROR, 429 ERR_SCHEMA_ELEM_NO_SPACE_AFTER_QUOTE.get(s)); 430 } 431 432 return pos; 433 } 434 else 435 { 436 throw new LDAPException(ResultCode.DECODING_ERROR, 437 ERR_SCHEMA_ELEM_EXPECTED_QUOTE_OR_PAREN.get(s, 438 startPos)); 439 } 440 } 441 442 443 444 /** 445 * Reads an OID value from the provided string. The OID value may be either a 446 * numeric OID or a string name. This implementation will be fairly lenient 447 * with regard to the set of characters that may be present, and it will 448 * allow the OID to be enclosed in single quotes. 449 * 450 * @param s The string from which to read the OID string. 451 * @param startPos The position at which to start reading. 452 * @param length The position of the end of the string. 453 * @param buffer The buffer into which the OID string should be placed. 454 * 455 * @return The position of the first space immediately following the OID 456 * string. 457 * 458 * @throws LDAPException If a problem is encountered while attempting to 459 * read the OID string. 460 */ 461 static int readOID(final String s, final int startPos, final int length, 462 final StringBuilder buffer) 463 throws LDAPException 464 { 465 // Read until we find the first space. 466 int pos = startPos; 467 boolean lastWasQuote = false; 468 while (pos < length) 469 { 470 final char c = s.charAt(pos); 471 if ((c == ' ') || (c == '$') || (c == ')')) 472 { 473 if (buffer.length() == 0) 474 { 475 throw new LDAPException(ResultCode.DECODING_ERROR, 476 ERR_SCHEMA_ELEM_EMPTY_OID.get(s)); 477 } 478 479 return pos; 480 } 481 else if (((c >= 'a') && (c <= 'z')) || 482 ((c >= 'A') && (c <= 'Z')) || 483 ((c >= '0') && (c <= '9')) || 484 (c == '-') || (c == '.') || (c == '_') || 485 (c == '{') || (c == '}')) 486 { 487 if (lastWasQuote) 488 { 489 throw new LDAPException(ResultCode.DECODING_ERROR, 490 ERR_SCHEMA_ELEM_UNEXPECTED_CHAR_IN_OID.get(s, (pos-1))); 491 } 492 493 buffer.append(c); 494 } 495 else if (c == '\'') 496 { 497 if (buffer.length() != 0) 498 { 499 lastWasQuote = true; 500 } 501 } 502 else 503 { 504 throw new LDAPException(ResultCode.DECODING_ERROR, 505 ERR_SCHEMA_ELEM_UNEXPECTED_CHAR_IN_OID.get(s, 506 pos)); 507 } 508 509 pos++; 510 } 511 512 513 // We hit the end of the string before finding a space. 514 throw new LDAPException(ResultCode.DECODING_ERROR, 515 ERR_SCHEMA_ELEM_NO_SPACE_AFTER_OID.get(s)); 516 } 517 518 519 520 /** 521 * Reads one a set of one or more OID strings from the provided string. The 522 * value to read may be either a single OID string or an opening parenthesis 523 * followed by a space followed by one or more space-delimited OID strings, 524 * followed by a space and a closing parenthesis. 525 * 526 * @param s The string from which to read the OID strings. 527 * @param startPos The position at which to start reading. 528 * @param length The position of the end of the string. 529 * @param valueList The list into which the values read may be placed. 530 * 531 * @return The position of the first space immediately following the end of 532 * the values. 533 * 534 * @throws LDAPException If a problem is encountered while attempting to 535 * read the OID strings. 536 */ 537 static int readOIDs(final String s, final int startPos, final int length, 538 final ArrayList<String> valueList) 539 throws LDAPException 540 { 541 // Look at the first character. If it's an opening parenthesis, then read 542 // a list of OID strings. Otherwise, just read a single string. 543 char c = s.charAt(startPos); 544 if (c == '(') 545 { 546 int pos = startPos + 1; 547 while (true) 548 { 549 pos = skipSpaces(s, pos, length); 550 c = s.charAt(pos); 551 if (c == ')') 552 { 553 // This is the end of the value list. 554 pos++; 555 break; 556 } 557 else if (c == '$') 558 { 559 // This is the delimiter before the next value in the list. 560 pos++; 561 pos = skipSpaces(s, pos, length); 562 final StringBuilder buffer = new StringBuilder(); 563 pos = readOID(s, pos, length, buffer); 564 valueList.add(buffer.toString()); 565 } 566 else if (valueList.isEmpty()) 567 { 568 // This is the first value in the list. 569 final StringBuilder buffer = new StringBuilder(); 570 pos = readOID(s, pos, length, buffer); 571 valueList.add(buffer.toString()); 572 } 573 else 574 { 575 throw new LDAPException(ResultCode.DECODING_ERROR, 576 ERR_SCHEMA_ELEM_UNEXPECTED_CHAR_IN_OID_LIST.get(s, 577 pos)); 578 } 579 } 580 581 if (valueList.isEmpty()) 582 { 583 throw new LDAPException(ResultCode.DECODING_ERROR, 584 ERR_SCHEMA_ELEM_EMPTY_OID_LIST.get(s)); 585 } 586 587 if (pos >= length) 588 { 589 // Technically, there should be a space after the closing parenthesis, 590 // but there are known cases in which servers (like Active Directory) 591 // omit this space, so we'll be lenient and allow a missing space. But 592 // it can't possibly be the end of the schema element definition, so 593 // that's still an error. 594 throw new LDAPException(ResultCode.DECODING_ERROR, 595 ERR_SCHEMA_ELEM_NO_SPACE_AFTER_OID_LIST.get(s)); 596 } 597 598 return pos; 599 } 600 else 601 { 602 final StringBuilder buffer = new StringBuilder(); 603 final int returnPos = readOID(s, startPos, length, buffer); 604 valueList.add(buffer.toString()); 605 return returnPos; 606 } 607 } 608 609 610 611 /** 612 * Appends a properly-encoded representation of the provided value to the 613 * given buffer. 614 * 615 * @param value The value to be encoded and placed in the buffer. 616 * @param buffer The buffer to which the encoded value is to be appended. 617 */ 618 static void encodeValue(final String value, final StringBuilder buffer) 619 { 620 final int length = value.length(); 621 for (int i=0; i < length; i++) 622 { 623 final char c = value.charAt(i); 624 if ((c < ' ') || (c > '~') || (c == '\\') || (c == '\'')) 625 { 626 hexEncode(c, buffer); 627 } 628 else 629 { 630 buffer.append(c); 631 } 632 } 633 } 634 635 636 637 /** 638 * Retrieves a hash code for this schema element. 639 * 640 * @return A hash code for this schema element. 641 */ 642 public abstract int hashCode(); 643 644 645 646 /** 647 * Indicates whether the provided object is equal to this schema element. 648 * 649 * @param o The object for which to make the determination. 650 * 651 * @return {@code true} if the provided object may be considered equal to 652 * this schema element, or {@code false} if not. 653 */ 654 public abstract boolean equals(final Object o); 655 656 657 658 /** 659 * Indicates whether the two extension maps are equivalent. 660 * 661 * @param m1 The first schema element to examine. 662 * @param m2 The second schema element to examine. 663 * 664 * @return {@code true} if the provided extension maps are equivalent, or 665 * {@code false} if not. 666 */ 667 protected static boolean extensionsEqual(final Map<String,String[]> m1, 668 final Map<String,String[]> m2) 669 { 670 if (m1.isEmpty()) 671 { 672 return m2.isEmpty(); 673 } 674 675 if (m1.size() != m2.size()) 676 { 677 return false; 678 } 679 680 for (final Map.Entry<String,String[]> e : m1.entrySet()) 681 { 682 final String[] v1 = e.getValue(); 683 final String[] v2 = m2.get(e.getKey()); 684 if (! arraysEqualOrderIndependent(v1, v2)) 685 { 686 return false; 687 } 688 } 689 690 return true; 691 } 692 693 694 695 /** 696 * Converts the provided collection of strings to an array. 697 * 698 * @param c The collection to convert to an array. It may be {@code null}. 699 * 700 * @return A string array if the provided collection is non-{@code null}, or 701 * {@code null} if the provided collection is {@code null}. 702 */ 703 static String[] toArray(final Collection<String> c) 704 { 705 if (c == null) 706 { 707 return null; 708 } 709 710 return c.toArray(NO_STRINGS); 711 } 712 713 714 715 /** 716 * Retrieves a string representation of this schema element, in the format 717 * described in RFC 4512. 718 * 719 * @return A string representation of this schema element, in the format 720 * described in RFC 4512. 721 */ 722 @Override() 723 public abstract String toString(); 724}