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 */
021package com.unboundid.ldap.sdk.schema;
022
023
024
025import java.io.Serializable;
026import java.nio.ByteBuffer;
027import java.util.ArrayList;
028import java.util.Map;
029
030import com.unboundid.ldap.sdk.LDAPException;
031import com.unboundid.ldap.sdk.ResultCode;
032import com.unboundid.util.NotExtensible;
033import com.unboundid.util.ThreadSafety;
034import com.unboundid.util.ThreadSafetyLevel;
035
036import static com.unboundid.ldap.sdk.schema.SchemaMessages.*;
037import static com.unboundid.util.Debug.*;
038import 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)
049public 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}