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 import com.unboundid.util.NotExtensible;
033 import com.unboundid.util.ThreadSafety;
034 import com.unboundid.util.ThreadSafetyLevel;
035
036 import static com.unboundid.ldap.sdk.schema.SchemaMessages.*;
037 import static com.unboundid.util.Debug.*;
038 import 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)
049 public 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 }