001/*
002 * Copyright 2007-2017 UnboundID Corp.
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2008-2017 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.Collection;
029import java.util.Map;
030
031import com.unboundid.ldap.sdk.LDAPException;
032import com.unboundid.ldap.sdk.ResultCode;
033import com.unboundid.util.NotExtensible;
034import com.unboundid.util.ThreadSafety;
035import com.unboundid.util.ThreadSafetyLevel;
036
037import static com.unboundid.ldap.sdk.schema.SchemaMessages.*;
038import static com.unboundid.util.Debug.*;
039import 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)
050public 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}