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 }