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.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 }