001/*
002 * Copyright 2007-2019 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2008-2019 Ping Identity Corporation
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.StaticUtils;
035import com.unboundid.util.ThreadSafety;
036import com.unboundid.util.ThreadSafetyLevel;
037
038import static com.unboundid.ldap.sdk.schema.SchemaMessages.*;
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      final 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          byteBuffer.put(b);
192          break;
193        case '1':
194          byteBuffer.put((byte) (b | 0x01));
195          break;
196        case '2':
197          byteBuffer.put((byte) (b | 0x02));
198          break;
199        case '3':
200          byteBuffer.put((byte) (b | 0x03));
201          break;
202        case '4':
203          byteBuffer.put((byte) (b | 0x04));
204          break;
205        case '5':
206          byteBuffer.put((byte) (b | 0x05));
207          break;
208        case '6':
209          byteBuffer.put((byte) (b | 0x06));
210          break;
211        case '7':
212          byteBuffer.put((byte) (b | 0x07));
213          break;
214        case '8':
215          byteBuffer.put((byte) (b | 0x08));
216          break;
217        case '9':
218          byteBuffer.put((byte) (b | 0x09));
219          break;
220        case 'a':
221        case 'A':
222          byteBuffer.put((byte) (b | 0x0A));
223          break;
224        case 'b':
225        case 'B':
226          byteBuffer.put((byte) (b | 0x0B));
227          break;
228        case 'c':
229        case 'C':
230          byteBuffer.put((byte) (b | 0x0C));
231          break;
232        case 'd':
233        case 'D':
234          byteBuffer.put((byte) (b | 0x0D));
235          break;
236        case 'e':
237        case 'E':
238          byteBuffer.put((byte) (b | 0x0E));
239          break;
240        case 'f':
241        case 'F':
242          byteBuffer.put((byte) (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      if (((pos+1) < length) && (s.charAt(pos) == '\\') &&
251          StaticUtils.isHex(s.charAt(pos+1)))
252      {
253        // It appears that there are more hex-encoded bytes to follow, so keep
254        // reading.
255        pos++;
256        continue;
257      }
258      else
259      {
260        break;
261      }
262    }
263
264    byteBuffer.flip();
265    final byte[] byteArray = new byte[byteBuffer.limit()];
266    byteBuffer.get(byteArray);
267    buffer.append(StaticUtils.toUTF8String(byteArray));
268    return pos;
269  }
270
271
272
273  /**
274   * Reads a single-quoted string from the provided string.
275   *
276   * @param  s         The string from which to read the single-quoted string.
277   * @param  startPos  The position at which to start reading.
278   * @param  length    The position of the end of the string.
279   * @param  buffer    The buffer into which the single-quoted string should be
280   *                   placed (without the surrounding single quotes).
281   *
282   * @return  The position of the first space immediately following the closing
283   *          quote.
284   *
285   * @throws  LDAPException  If a problem is encountered while attempting to
286   *                         read the single-quoted string.
287   */
288  static int readQDString(final String s, final int startPos, final int length,
289                          final StringBuilder buffer)
290      throws LDAPException
291  {
292    // The first character must be a single quote.
293    if (s.charAt(startPos) != '\'')
294    {
295      throw new LDAPException(ResultCode.DECODING_ERROR,
296                              ERR_SCHEMA_ELEM_EXPECTED_SINGLE_QUOTE.get(s,
297                                   startPos));
298    }
299
300    // Read until we find the next closing quote.  If we find any hex-escaped
301    // characters along the way, then decode them.
302    int pos = startPos + 1;
303    while (pos < length)
304    {
305      final char c = s.charAt(pos++);
306      if (c == '\'')
307      {
308        // This is the end of the quoted string.
309        break;
310      }
311      else if (c == '\\')
312      {
313        // This designates the beginning of one or more hex-encoded bytes.
314        if (pos >= length)
315        {
316          throw new LDAPException(ResultCode.DECODING_ERROR,
317                                  ERR_SCHEMA_ELEM_ENDS_WITH_BACKSLASH.get(s));
318        }
319
320        pos = readEscapedHexString(s, pos, length, buffer);
321      }
322      else
323      {
324        buffer.append(c);
325      }
326    }
327
328    if ((pos >= length) || ((s.charAt(pos) != ' ') && (s.charAt(pos) != ')')))
329    {
330      throw new LDAPException(ResultCode.DECODING_ERROR,
331                              ERR_SCHEMA_ELEM_NO_CLOSING_PAREN.get(s));
332    }
333
334    if (buffer.length() == 0)
335    {
336      throw new LDAPException(ResultCode.DECODING_ERROR,
337                              ERR_SCHEMA_ELEM_EMPTY_QUOTES.get(s));
338    }
339
340    return pos;
341  }
342
343
344
345  /**
346   * Reads one a set of one or more single-quoted strings from the provided
347   * string.  The value to read may be either a single string enclosed in
348   * single quotes, or an opening parenthesis followed by a space followed by
349   * one or more space-delimited single-quoted strings, followed by a space and
350   * a closing parenthesis.
351   *
352   * @param  s          The string from which to read the single-quoted strings.
353   * @param  startPos   The position at which to start reading.
354   * @param  length     The position of the end of the string.
355   * @param  valueList  The list into which the values read may be placed.
356   *
357   * @return  The position of the first space immediately following the end of
358   *          the values.
359   *
360   * @throws  LDAPException  If a problem is encountered while attempting to
361   *                         read the single-quoted strings.
362   */
363  static int readQDStrings(final String s, final int startPos, final int length,
364                           final ArrayList<String> valueList)
365      throws LDAPException
366  {
367    // Look at the first character.  It must be either a single quote or an
368    // opening parenthesis.
369    char c = s.charAt(startPos);
370    if (c == '\'')
371    {
372      // It's just a single value, so use the readQDString method to get it.
373      final StringBuilder buffer = new StringBuilder();
374      final int returnPos = readQDString(s, startPos, length, buffer);
375      valueList.add(buffer.toString());
376      return returnPos;
377    }
378    else if (c == '(')
379    {
380      int pos = startPos + 1;
381      while (true)
382      {
383        pos = skipSpaces(s, pos, length);
384        c = s.charAt(pos);
385        if (c == ')')
386        {
387          // This is the end of the value list.
388          pos++;
389          break;
390        }
391        else if (c == '\'')
392        {
393          // This is the next value in the list.
394          final StringBuilder buffer = new StringBuilder();
395          pos = readQDString(s, pos, length, buffer);
396          valueList.add(buffer.toString());
397        }
398        else
399        {
400          throw new LDAPException(ResultCode.DECODING_ERROR,
401                                  ERR_SCHEMA_ELEM_EXPECTED_QUOTE_OR_PAREN.get(
402                                       s, startPos));
403        }
404      }
405
406      if (valueList.isEmpty())
407      {
408        throw new LDAPException(ResultCode.DECODING_ERROR,
409                                ERR_SCHEMA_ELEM_EMPTY_STRING_LIST.get(s));
410      }
411
412      if ((pos >= length) ||
413          ((s.charAt(pos) != ' ') && (s.charAt(pos) != ')')))
414      {
415        throw new LDAPException(ResultCode.DECODING_ERROR,
416                                ERR_SCHEMA_ELEM_NO_SPACE_AFTER_QUOTE.get(s));
417      }
418
419      return pos;
420    }
421    else
422    {
423      throw new LDAPException(ResultCode.DECODING_ERROR,
424                              ERR_SCHEMA_ELEM_EXPECTED_QUOTE_OR_PAREN.get(s,
425                                   startPos));
426    }
427  }
428
429
430
431  /**
432   * Reads an OID value from the provided string.  The OID value may be either a
433   * numeric OID or a string name.  This implementation will be fairly lenient
434   * with regard to the set of characters that may be present, and it will
435   * allow the OID to be enclosed in single quotes.
436   *
437   * @param  s         The string from which to read the OID string.
438   * @param  startPos  The position at which to start reading.
439   * @param  length    The position of the end of the string.
440   * @param  buffer    The buffer into which the OID string should be placed.
441   *
442   * @return  The position of the first space immediately following the OID
443   *          string.
444   *
445   * @throws  LDAPException  If a problem is encountered while attempting to
446   *                         read the OID string.
447   */
448  static int readOID(final String s, final int startPos, final int length,
449                     final StringBuilder buffer)
450      throws LDAPException
451  {
452    // Read until we find the first space.
453    int pos = startPos;
454    boolean lastWasQuote = false;
455    while (pos < length)
456    {
457      final char c = s.charAt(pos);
458      if ((c == ' ') || (c == '$') || (c == ')'))
459      {
460        if (buffer.length() == 0)
461        {
462          throw new LDAPException(ResultCode.DECODING_ERROR,
463                                  ERR_SCHEMA_ELEM_EMPTY_OID.get(s));
464        }
465
466        return pos;
467      }
468      else if (((c >= 'a') && (c <= 'z')) ||
469               ((c >= 'A') && (c <= 'Z')) ||
470               ((c >= '0') && (c <= '9')) ||
471               (c == '-') || (c == '.') || (c == '_') ||
472               (c == '{') || (c == '}'))
473      {
474        if (lastWasQuote)
475        {
476          throw new LDAPException(ResultCode.DECODING_ERROR,
477               ERR_SCHEMA_ELEM_UNEXPECTED_CHAR_IN_OID.get(s, (pos-1)));
478        }
479
480        buffer.append(c);
481      }
482      else if (c == '\'')
483      {
484        if (buffer.length() != 0)
485        {
486          lastWasQuote = true;
487        }
488      }
489      else
490      {
491          throw new LDAPException(ResultCode.DECODING_ERROR,
492                                  ERR_SCHEMA_ELEM_UNEXPECTED_CHAR_IN_OID.get(s,
493                                       pos));
494      }
495
496      pos++;
497    }
498
499
500    // We hit the end of the string before finding a space.
501    throw new LDAPException(ResultCode.DECODING_ERROR,
502                            ERR_SCHEMA_ELEM_NO_SPACE_AFTER_OID.get(s));
503  }
504
505
506
507  /**
508   * Reads one a set of one or more OID strings from the provided string.  The
509   * value to read may be either a single OID string or an opening parenthesis
510   * followed by a space followed by one or more space-delimited OID strings,
511   * followed by a space and a closing parenthesis.
512   *
513   * @param  s          The string from which to read the OID strings.
514   * @param  startPos   The position at which to start reading.
515   * @param  length     The position of the end of the string.
516   * @param  valueList  The list into which the values read may be placed.
517   *
518   * @return  The position of the first space immediately following the end of
519   *          the values.
520   *
521   * @throws  LDAPException  If a problem is encountered while attempting to
522   *                         read the OID strings.
523   */
524  static int readOIDs(final String s, final int startPos, final int length,
525                      final ArrayList<String> valueList)
526      throws LDAPException
527  {
528    // Look at the first character.  If it's an opening parenthesis, then read
529    // a list of OID strings.  Otherwise, just read a single string.
530    char c = s.charAt(startPos);
531    if (c == '(')
532    {
533      int pos = startPos + 1;
534      while (true)
535      {
536        pos = skipSpaces(s, pos, length);
537        c = s.charAt(pos);
538        if (c == ')')
539        {
540          // This is the end of the value list.
541          pos++;
542          break;
543        }
544        else if (c == '$')
545        {
546          // This is the delimiter before the next value in the list.
547          pos++;
548          pos = skipSpaces(s, pos, length);
549          final StringBuilder buffer = new StringBuilder();
550          pos = readOID(s, pos, length, buffer);
551          valueList.add(buffer.toString());
552        }
553        else if (valueList.isEmpty())
554        {
555          // This is the first value in the list.
556          final StringBuilder buffer = new StringBuilder();
557          pos = readOID(s, pos, length, buffer);
558          valueList.add(buffer.toString());
559        }
560        else
561        {
562          throw new LDAPException(ResultCode.DECODING_ERROR,
563                         ERR_SCHEMA_ELEM_UNEXPECTED_CHAR_IN_OID_LIST.get(s,
564                              pos));
565        }
566      }
567
568      if (valueList.isEmpty())
569      {
570        throw new LDAPException(ResultCode.DECODING_ERROR,
571                                ERR_SCHEMA_ELEM_EMPTY_OID_LIST.get(s));
572      }
573
574      if (pos >= length)
575      {
576        // Technically, there should be a space after the closing parenthesis,
577        // but there are known cases in which servers (like Active Directory)
578        // omit this space, so we'll be lenient and allow a missing space.  But
579        // it can't possibly be the end of the schema element definition, so
580        // that's still an error.
581        throw new LDAPException(ResultCode.DECODING_ERROR,
582                                ERR_SCHEMA_ELEM_NO_SPACE_AFTER_OID_LIST.get(s));
583      }
584
585      return pos;
586    }
587    else
588    {
589      final StringBuilder buffer = new StringBuilder();
590      final int returnPos = readOID(s, startPos, length, buffer);
591      valueList.add(buffer.toString());
592      return returnPos;
593    }
594  }
595
596
597
598  /**
599   * Appends a properly-encoded representation of the provided value to the
600   * given buffer.
601   *
602   * @param  value   The value to be encoded and placed in the buffer.
603   * @param  buffer  The buffer to which the encoded value is to be appended.
604   */
605  static void encodeValue(final String value, final StringBuilder buffer)
606  {
607    final int length = value.length();
608    for (int i=0; i < length; i++)
609    {
610      final char c = value.charAt(i);
611      if ((c < ' ') || (c > '~') || (c == '\\') || (c == '\''))
612      {
613        StaticUtils.hexEncode(c, buffer);
614      }
615      else
616      {
617        buffer.append(c);
618      }
619    }
620  }
621
622
623
624  /**
625   * Retrieves a hash code for this schema element.
626   *
627   * @return  A hash code for this schema element.
628   */
629  public abstract int hashCode();
630
631
632
633  /**
634   * Indicates whether the provided object is equal to this schema element.
635   *
636   * @param  o  The object for which to make the determination.
637   *
638   * @return  {@code true} if the provided object may be considered equal to
639   *          this schema element, or {@code false} if not.
640   */
641  public abstract boolean equals(Object o);
642
643
644
645  /**
646   * Indicates whether the two extension maps are equivalent.
647   *
648   * @param  m1  The first schema element to examine.
649   * @param  m2  The second schema element to examine.
650   *
651   * @return  {@code true} if the provided extension maps are equivalent, or
652   *          {@code false} if not.
653   */
654  protected static boolean extensionsEqual(final Map<String,String[]> m1,
655                                           final Map<String,String[]> m2)
656  {
657    if (m1.isEmpty())
658    {
659      return m2.isEmpty();
660    }
661
662    if (m1.size() != m2.size())
663    {
664      return false;
665    }
666
667    for (final Map.Entry<String,String[]> e : m1.entrySet())
668    {
669      final String[] v1 = e.getValue();
670      final String[] v2 = m2.get(e.getKey());
671      if (! StaticUtils.arraysEqualOrderIndependent(v1, v2))
672      {
673        return false;
674      }
675    }
676
677    return true;
678  }
679
680
681
682  /**
683   * Converts the provided collection of strings to an array.
684   *
685   * @param  c  The collection to convert to an array.  It may be {@code null}.
686   *
687   * @return  A string array if the provided collection is non-{@code null}, or
688   *          {@code null} if the provided collection is {@code null}.
689   */
690  static String[] toArray(final Collection<String> c)
691  {
692    if (c == null)
693    {
694      return null;
695    }
696
697    return c.toArray(StaticUtils.NO_STRINGS);
698  }
699
700
701
702  /**
703   * Retrieves a string representation of this schema element, in the format
704   * described in RFC 4512.
705   *
706   * @return  A string representation of this schema element, in the format
707   *          described in RFC 4512.
708   */
709  @Override()
710  public abstract String toString();
711}