001/* 002 * Copyright 2007-2014 UnboundID Corp. 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2008-2014 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.Map; 029 030import com.unboundid.ldap.sdk.LDAPException; 031import com.unboundid.ldap.sdk.ResultCode; 032import com.unboundid.util.NotExtensible; 033import com.unboundid.util.ThreadSafety; 034import com.unboundid.util.ThreadSafetyLevel; 035 036import static com.unboundid.ldap.sdk.schema.SchemaMessages.*; 037import static com.unboundid.util.Debug.*; 038import static com.unboundid.util.StaticUtils.*; 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 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 // No action is required. 192 break; 193 case '1': 194 b |= 0x01; 195 break; 196 case '2': 197 b |= 0x02; 198 break; 199 case '3': 200 b |= 0x03; 201 break; 202 case '4': 203 b |= 0x04; 204 break; 205 case '5': 206 b |= 0x05; 207 break; 208 case '6': 209 b |= 0x06; 210 break; 211 case '7': 212 b |= 0x07; 213 break; 214 case '8': 215 b |= 0x08; 216 break; 217 case '9': 218 b |= 0x09; 219 break; 220 case 'a': 221 case 'A': 222 b |= 0x0A; 223 break; 224 case 'b': 225 case 'B': 226 b |= 0x0B; 227 break; 228 case 'c': 229 case 'C': 230 b |= 0x0C; 231 break; 232 case 'd': 233 case 'D': 234 b |= 0x0D; 235 break; 236 case 'e': 237 case 'E': 238 b |= 0x0E; 239 break; 240 case 'f': 241 case 'F': 242 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 byteBuffer.put(b); 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(final 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 * Retrieves a string representation of this schema element, in the format 696 * described in RFC 4512. 697 * 698 * @return A string representation of this schema element, in the format 699 * described in RFC 4512. 700 */ 701 @Override() 702 public abstract String toString(); 703}