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