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