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