001/*
002 * Copyright 2007-2018 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2008-2018 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.Debug;
034import com.unboundid.util.NotExtensible;
035import com.unboundid.util.StaticUtils;
036import com.unboundid.util.ThreadSafety;
037import com.unboundid.util.ThreadSafetyLevel;
038
039import static com.unboundid.ldap.sdk.schema.SchemaMessages.*;
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      final 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          byteBuffer.put(b);
193          break;
194        case '1':
195          byteBuffer.put((byte) (b | 0x01));
196          break;
197        case '2':
198          byteBuffer.put((byte) (b | 0x02));
199          break;
200        case '3':
201          byteBuffer.put((byte) (b | 0x03));
202          break;
203        case '4':
204          byteBuffer.put((byte) (b | 0x04));
205          break;
206        case '5':
207          byteBuffer.put((byte) (b | 0x05));
208          break;
209        case '6':
210          byteBuffer.put((byte) (b | 0x06));
211          break;
212        case '7':
213          byteBuffer.put((byte) (b | 0x07));
214          break;
215        case '8':
216          byteBuffer.put((byte) (b | 0x08));
217          break;
218        case '9':
219          byteBuffer.put((byte) (b | 0x09));
220          break;
221        case 'a':
222        case 'A':
223          byteBuffer.put((byte) (b | 0x0A));
224          break;
225        case 'b':
226        case 'B':
227          byteBuffer.put((byte) (b | 0x0B));
228          break;
229        case 'c':
230        case 'C':
231          byteBuffer.put((byte) (b | 0x0C));
232          break;
233        case 'd':
234        case 'D':
235          byteBuffer.put((byte) (b | 0x0D));
236          break;
237        case 'e':
238        case 'E':
239          byteBuffer.put((byte) (b | 0x0E));
240          break;
241        case 'f':
242        case 'F':
243          byteBuffer.put((byte) (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      if (((pos+1) < length) && (s.charAt(pos) == '\\') &&
252          StaticUtils.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(StaticUtils.toUTF8String(byteArray));
272    }
273    catch (final Exception e)
274    {
275      Debug.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        StaticUtils.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(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 (! StaticUtils.arraysEqualOrderIndependent(v1, v2))
684      {
685        return false;
686      }
687    }
688
689    return true;
690  }
691
692
693
694  /**
695   * Converts the provided collection of strings to an array.
696   *
697   * @param  c  The collection to convert to an array.  It may be {@code null}.
698   *
699   * @return  A string array if the provided collection is non-{@code null}, or
700   *          {@code null} if the provided collection is {@code null}.
701   */
702  static String[] toArray(final Collection<String> c)
703  {
704    if (c == null)
705    {
706      return null;
707    }
708
709    return c.toArray(StaticUtils.NO_STRINGS);
710  }
711
712
713
714  /**
715   * Retrieves a string representation of this schema element, in the format
716   * described in RFC 4512.
717   *
718   * @return  A string representation of this schema element, in the format
719   *          described in RFC 4512.
720   */
721  @Override()
722  public abstract String toString();
723}