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