001/*
002 * Copyright 2007-2020 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2007-2020 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2008-2020 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldap.sdk.schema;
037
038
039
040import java.io.Serializable;
041import java.nio.ByteBuffer;
042import java.util.ArrayList;
043import java.util.Collection;
044import java.util.Map;
045
046import com.unboundid.ldap.sdk.LDAPException;
047import com.unboundid.ldap.sdk.ResultCode;
048import com.unboundid.util.NotExtensible;
049import com.unboundid.util.StaticUtils;
050import com.unboundid.util.ThreadSafety;
051import com.unboundid.util.ThreadSafetyLevel;
052
053import static com.unboundid.ldap.sdk.schema.SchemaMessages.*;
054
055
056
057/**
058 * This class provides a superclass for all schema element types, and defines a
059 * number of utility methods that may be used when parsing schema element
060 * strings.
061 */
062@NotExtensible()
063@ThreadSafety(level=ThreadSafetyLevel.INTERFACE_THREADSAFE)
064public abstract class SchemaElement
065       implements Serializable
066{
067  /**
068   * The serial version UID for this serializable class.
069   */
070  private static final long serialVersionUID = -8249972237068748580L;
071
072
073
074  /**
075   * Skips over any any spaces in the provided string.
076   *
077   * @param  s         The string in which to skip the spaces.
078   * @param  startPos  The position at which to start skipping spaces.
079   * @param  length    The position of the end of the string.
080   *
081   * @return  The position of the next non-space character in the string.
082   *
083   * @throws  LDAPException  If the end of the string was reached without
084   *                         finding a non-space character.
085   */
086  static int skipSpaces(final String s, final int startPos, final int length)
087         throws LDAPException
088  {
089    int pos = startPos;
090    while ((pos < length) && (s.charAt(pos) == ' '))
091    {
092      pos++;
093    }
094
095    if (pos >= length)
096    {
097      throw new LDAPException(ResultCode.DECODING_ERROR,
098                              ERR_SCHEMA_ELEM_SKIP_SPACES_NO_CLOSE_PAREN.get(
099                                   s));
100    }
101
102    return pos;
103  }
104
105
106
107  /**
108   * Reads one or more hex-encoded bytes from the specified portion of the RDN
109   * string.
110   *
111   * @param  s         The string from which the data is to be read.
112   * @param  startPos  The position at which to start reading.  This should be
113   *                   the first hex character immediately after the initial
114   *                   backslash.
115   * @param  length    The position of the end of the string.
116   * @param  buffer    The buffer to which the decoded string portion should be
117   *                   appended.
118   *
119   * @return  The position at which the caller may resume parsing.
120   *
121   * @throws  LDAPException  If a problem occurs while reading hex-encoded
122   *                         bytes.
123   */
124  private static int readEscapedHexString(final String s, final int startPos,
125                                          final int length,
126                                          final StringBuilder buffer)
127          throws LDAPException
128  {
129    int pos    = startPos;
130
131    final ByteBuffer byteBuffer = ByteBuffer.allocate(length - pos);
132    while (pos < length)
133    {
134      final byte b;
135      switch (s.charAt(pos++))
136      {
137        case '0':
138          b = 0x00;
139          break;
140        case '1':
141          b = 0x10;
142          break;
143        case '2':
144          b = 0x20;
145          break;
146        case '3':
147          b = 0x30;
148          break;
149        case '4':
150          b = 0x40;
151          break;
152        case '5':
153          b = 0x50;
154          break;
155        case '6':
156          b = 0x60;
157          break;
158        case '7':
159          b = 0x70;
160          break;
161        case '8':
162          b = (byte) 0x80;
163          break;
164        case '9':
165          b = (byte) 0x90;
166          break;
167        case 'a':
168        case 'A':
169          b = (byte) 0xA0;
170          break;
171        case 'b':
172        case 'B':
173          b = (byte) 0xB0;
174          break;
175        case 'c':
176        case 'C':
177          b = (byte) 0xC0;
178          break;
179        case 'd':
180        case 'D':
181          b = (byte) 0xD0;
182          break;
183        case 'e':
184        case 'E':
185          b = (byte) 0xE0;
186          break;
187        case 'f':
188        case 'F':
189          b = (byte) 0xF0;
190          break;
191        default:
192          throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
193                                  ERR_SCHEMA_ELEM_INVALID_HEX_CHAR.get(s,
194                                       s.charAt(pos-1), (pos-1)));
195      }
196
197      if (pos >= length)
198      {
199        throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
200                                ERR_SCHEMA_ELEM_MISSING_HEX_CHAR.get(s));
201      }
202
203      switch (s.charAt(pos++))
204      {
205        case '0':
206          byteBuffer.put(b);
207          break;
208        case '1':
209          byteBuffer.put((byte) (b | 0x01));
210          break;
211        case '2':
212          byteBuffer.put((byte) (b | 0x02));
213          break;
214        case '3':
215          byteBuffer.put((byte) (b | 0x03));
216          break;
217        case '4':
218          byteBuffer.put((byte) (b | 0x04));
219          break;
220        case '5':
221          byteBuffer.put((byte) (b | 0x05));
222          break;
223        case '6':
224          byteBuffer.put((byte) (b | 0x06));
225          break;
226        case '7':
227          byteBuffer.put((byte) (b | 0x07));
228          break;
229        case '8':
230          byteBuffer.put((byte) (b | 0x08));
231          break;
232        case '9':
233          byteBuffer.put((byte) (b | 0x09));
234          break;
235        case 'a':
236        case 'A':
237          byteBuffer.put((byte) (b | 0x0A));
238          break;
239        case 'b':
240        case 'B':
241          byteBuffer.put((byte) (b | 0x0B));
242          break;
243        case 'c':
244        case 'C':
245          byteBuffer.put((byte) (b | 0x0C));
246          break;
247        case 'd':
248        case 'D':
249          byteBuffer.put((byte) (b | 0x0D));
250          break;
251        case 'e':
252        case 'E':
253          byteBuffer.put((byte) (b | 0x0E));
254          break;
255        case 'f':
256        case 'F':
257          byteBuffer.put((byte) (b | 0x0F));
258          break;
259        default:
260          throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
261                                  ERR_SCHEMA_ELEM_INVALID_HEX_CHAR.get(s,
262                                       s.charAt(pos-1), (pos-1)));
263      }
264
265      if (((pos+1) < length) && (s.charAt(pos) == '\\') &&
266          StaticUtils.isHex(s.charAt(pos+1)))
267      {
268        // It appears that there are more hex-encoded bytes to follow, so keep
269        // reading.
270        pos++;
271        continue;
272      }
273      else
274      {
275        break;
276      }
277    }
278
279    byteBuffer.flip();
280    final byte[] byteArray = new byte[byteBuffer.limit()];
281    byteBuffer.get(byteArray);
282    buffer.append(StaticUtils.toUTF8String(byteArray));
283    return pos;
284  }
285
286
287
288  /**
289   * Reads a single-quoted string from the provided string.
290   *
291   * @param  s         The string from which to read the single-quoted string.
292   * @param  startPos  The position at which to start reading.
293   * @param  length    The position of the end of the string.
294   * @param  buffer    The buffer into which the single-quoted string should be
295   *                   placed (without the surrounding single quotes).
296   *
297   * @return  The position of the first space immediately following the closing
298   *          quote.
299   *
300   * @throws  LDAPException  If a problem is encountered while attempting to
301   *                         read the single-quoted string.
302   */
303  static int readQDString(final String s, final int startPos, final int length,
304                          final StringBuilder buffer)
305      throws LDAPException
306  {
307    // The first character must be a single quote.
308    if (s.charAt(startPos) != '\'')
309    {
310      throw new LDAPException(ResultCode.DECODING_ERROR,
311                              ERR_SCHEMA_ELEM_EXPECTED_SINGLE_QUOTE.get(s,
312                                   startPos));
313    }
314
315    // Read until we find the next closing quote.  If we find any hex-escaped
316    // characters along the way, then decode them.
317    int pos = startPos + 1;
318    while (pos < length)
319    {
320      final char c = s.charAt(pos++);
321      if (c == '\'')
322      {
323        // This is the end of the quoted string.
324        break;
325      }
326      else if (c == '\\')
327      {
328        // This designates the beginning of one or more hex-encoded bytes.
329        if (pos >= length)
330        {
331          throw new LDAPException(ResultCode.DECODING_ERROR,
332                                  ERR_SCHEMA_ELEM_ENDS_WITH_BACKSLASH.get(s));
333        }
334
335        pos = readEscapedHexString(s, pos, length, buffer);
336      }
337      else
338      {
339        buffer.append(c);
340      }
341    }
342
343    if ((pos >= length) || ((s.charAt(pos) != ' ') && (s.charAt(pos) != ')')))
344    {
345      throw new LDAPException(ResultCode.DECODING_ERROR,
346                              ERR_SCHEMA_ELEM_NO_CLOSING_PAREN.get(s));
347    }
348
349    if (buffer.length() == 0)
350    {
351      throw new LDAPException(ResultCode.DECODING_ERROR,
352                              ERR_SCHEMA_ELEM_EMPTY_QUOTES.get(s));
353    }
354
355    return pos;
356  }
357
358
359
360  /**
361   * Reads one a set of one or more single-quoted strings from the provided
362   * string.  The value to read may be either a single string enclosed in
363   * single quotes, or an opening parenthesis followed by a space followed by
364   * one or more space-delimited single-quoted strings, followed by a space and
365   * a closing parenthesis.
366   *
367   * @param  s          The string from which to read the single-quoted strings.
368   * @param  startPos   The position at which to start reading.
369   * @param  length     The position of the end of the string.
370   * @param  valueList  The list into which the values read may be placed.
371   *
372   * @return  The position of the first space immediately following the end of
373   *          the values.
374   *
375   * @throws  LDAPException  If a problem is encountered while attempting to
376   *                         read the single-quoted strings.
377   */
378  static int readQDStrings(final String s, final int startPos, final int length,
379                           final ArrayList<String> valueList)
380      throws LDAPException
381  {
382    // Look at the first character.  It must be either a single quote or an
383    // opening parenthesis.
384    char c = s.charAt(startPos);
385    if (c == '\'')
386    {
387      // It's just a single value, so use the readQDString method to get it.
388      final StringBuilder buffer = new StringBuilder();
389      final int returnPos = readQDString(s, startPos, length, buffer);
390      valueList.add(buffer.toString());
391      return returnPos;
392    }
393    else if (c == '(')
394    {
395      int pos = startPos + 1;
396      while (true)
397      {
398        pos = skipSpaces(s, pos, length);
399        c = s.charAt(pos);
400        if (c == ')')
401        {
402          // This is the end of the value list.
403          pos++;
404          break;
405        }
406        else if (c == '\'')
407        {
408          // This is the next value in the list.
409          final StringBuilder buffer = new StringBuilder();
410          pos = readQDString(s, pos, length, buffer);
411          valueList.add(buffer.toString());
412        }
413        else
414        {
415          throw new LDAPException(ResultCode.DECODING_ERROR,
416                                  ERR_SCHEMA_ELEM_EXPECTED_QUOTE_OR_PAREN.get(
417                                       s, startPos));
418        }
419      }
420
421      if (valueList.isEmpty())
422      {
423        throw new LDAPException(ResultCode.DECODING_ERROR,
424                                ERR_SCHEMA_ELEM_EMPTY_STRING_LIST.get(s));
425      }
426
427      if ((pos >= length) ||
428          ((s.charAt(pos) != ' ') && (s.charAt(pos) != ')')))
429      {
430        throw new LDAPException(ResultCode.DECODING_ERROR,
431                                ERR_SCHEMA_ELEM_NO_SPACE_AFTER_QUOTE.get(s));
432      }
433
434      return pos;
435    }
436    else
437    {
438      throw new LDAPException(ResultCode.DECODING_ERROR,
439                              ERR_SCHEMA_ELEM_EXPECTED_QUOTE_OR_PAREN.get(s,
440                                   startPos));
441    }
442  }
443
444
445
446  /**
447   * Reads an OID value from the provided string.  The OID value may be either a
448   * numeric OID or a string name.  This implementation will be fairly lenient
449   * with regard to the set of characters that may be present, and it will
450   * allow the OID to be enclosed in single quotes.
451   *
452   * @param  s         The string from which to read the OID string.
453   * @param  startPos  The position at which to start reading.
454   * @param  length    The position of the end of the string.
455   * @param  buffer    The buffer into which the OID string should be placed.
456   *
457   * @return  The position of the first space immediately following the OID
458   *          string.
459   *
460   * @throws  LDAPException  If a problem is encountered while attempting to
461   *                         read the OID string.
462   */
463  static int readOID(final String s, final int startPos, final int length,
464                     final StringBuilder buffer)
465      throws LDAPException
466  {
467    // Read until we find the first space.
468    int pos = startPos;
469    boolean lastWasQuote = false;
470    while (pos < length)
471    {
472      final char c = s.charAt(pos);
473      if ((c == ' ') || (c == '$') || (c == ')'))
474      {
475        if (buffer.length() == 0)
476        {
477          throw new LDAPException(ResultCode.DECODING_ERROR,
478                                  ERR_SCHEMA_ELEM_EMPTY_OID.get(s));
479        }
480
481        return pos;
482      }
483      else if (((c >= 'a') && (c <= 'z')) ||
484               ((c >= 'A') && (c <= 'Z')) ||
485               ((c >= '0') && (c <= '9')) ||
486               (c == '-') || (c == '.') || (c == '_') ||
487               (c == '{') || (c == '}'))
488      {
489        if (lastWasQuote)
490        {
491          throw new LDAPException(ResultCode.DECODING_ERROR,
492               ERR_SCHEMA_ELEM_UNEXPECTED_CHAR_IN_OID.get(s, (pos-1)));
493        }
494
495        buffer.append(c);
496      }
497      else if (c == '\'')
498      {
499        if (buffer.length() != 0)
500        {
501          lastWasQuote = true;
502        }
503      }
504      else
505      {
506          throw new LDAPException(ResultCode.DECODING_ERROR,
507                                  ERR_SCHEMA_ELEM_UNEXPECTED_CHAR_IN_OID.get(s,
508                                       pos));
509      }
510
511      pos++;
512    }
513
514
515    // We hit the end of the string before finding a space.
516    throw new LDAPException(ResultCode.DECODING_ERROR,
517                            ERR_SCHEMA_ELEM_NO_SPACE_AFTER_OID.get(s));
518  }
519
520
521
522  /**
523   * Reads one a set of one or more OID strings from the provided string.  The
524   * value to read may be either a single OID string or an opening parenthesis
525   * followed by a space followed by one or more space-delimited OID strings,
526   * followed by a space and a closing parenthesis.
527   *
528   * @param  s          The string from which to read the OID strings.
529   * @param  startPos   The position at which to start reading.
530   * @param  length     The position of the end of the string.
531   * @param  valueList  The list into which the values read may be placed.
532   *
533   * @return  The position of the first space immediately following the end of
534   *          the values.
535   *
536   * @throws  LDAPException  If a problem is encountered while attempting to
537   *                         read the OID strings.
538   */
539  static int readOIDs(final String s, final int startPos, final int length,
540                      final ArrayList<String> valueList)
541      throws LDAPException
542  {
543    // Look at the first character.  If it's an opening parenthesis, then read
544    // a list of OID strings.  Otherwise, just read a single string.
545    char c = s.charAt(startPos);
546    if (c == '(')
547    {
548      int pos = startPos + 1;
549      while (true)
550      {
551        pos = skipSpaces(s, pos, length);
552        c = s.charAt(pos);
553        if (c == ')')
554        {
555          // This is the end of the value list.
556          pos++;
557          break;
558        }
559        else if (c == '$')
560        {
561          // This is the delimiter before the next value in the list.
562          pos++;
563          pos = skipSpaces(s, pos, length);
564          final StringBuilder buffer = new StringBuilder();
565          pos = readOID(s, pos, length, buffer);
566          valueList.add(buffer.toString());
567        }
568        else if (valueList.isEmpty())
569        {
570          // This is the first value in the list.
571          final StringBuilder buffer = new StringBuilder();
572          pos = readOID(s, pos, length, buffer);
573          valueList.add(buffer.toString());
574        }
575        else
576        {
577          throw new LDAPException(ResultCode.DECODING_ERROR,
578                         ERR_SCHEMA_ELEM_UNEXPECTED_CHAR_IN_OID_LIST.get(s,
579                              pos));
580        }
581      }
582
583      if (valueList.isEmpty())
584      {
585        throw new LDAPException(ResultCode.DECODING_ERROR,
586                                ERR_SCHEMA_ELEM_EMPTY_OID_LIST.get(s));
587      }
588
589      if (pos >= length)
590      {
591        // Technically, there should be a space after the closing parenthesis,
592        // but there are known cases in which servers (like Active Directory)
593        // omit this space, so we'll be lenient and allow a missing space.  But
594        // it can't possibly be the end of the schema element definition, so
595        // that's still an error.
596        throw new LDAPException(ResultCode.DECODING_ERROR,
597                                ERR_SCHEMA_ELEM_NO_SPACE_AFTER_OID_LIST.get(s));
598      }
599
600      return pos;
601    }
602    else
603    {
604      final StringBuilder buffer = new StringBuilder();
605      final int returnPos = readOID(s, startPos, length, buffer);
606      valueList.add(buffer.toString());
607      return returnPos;
608    }
609  }
610
611
612
613  /**
614   * Appends a properly-encoded representation of the provided value to the
615   * given buffer.
616   *
617   * @param  value   The value to be encoded and placed in the buffer.
618   * @param  buffer  The buffer to which the encoded value is to be appended.
619   */
620  static void encodeValue(final String value, final StringBuilder buffer)
621  {
622    final int length = value.length();
623    for (int i=0; i < length; i++)
624    {
625      final char c = value.charAt(i);
626      if ((c < ' ') || (c > '~') || (c == '\\') || (c == '\''))
627      {
628        StaticUtils.hexEncode(c, buffer);
629      }
630      else
631      {
632        buffer.append(c);
633      }
634    }
635  }
636
637
638
639  /**
640   * Retrieves a hash code for this schema element.
641   *
642   * @return  A hash code for this schema element.
643   */
644  public abstract int hashCode();
645
646
647
648  /**
649   * Indicates whether the provided object is equal to this schema element.
650   *
651   * @param  o  The object for which to make the determination.
652   *
653   * @return  {@code true} if the provided object may be considered equal to
654   *          this schema element, or {@code false} if not.
655   */
656  public abstract boolean equals(Object o);
657
658
659
660  /**
661   * Indicates whether the two extension maps are equivalent.
662   *
663   * @param  m1  The first schema element to examine.
664   * @param  m2  The second schema element to examine.
665   *
666   * @return  {@code true} if the provided extension maps are equivalent, or
667   *          {@code false} if not.
668   */
669  protected static boolean extensionsEqual(final Map<String,String[]> m1,
670                                           final Map<String,String[]> m2)
671  {
672    if (m1.isEmpty())
673    {
674      return m2.isEmpty();
675    }
676
677    if (m1.size() != m2.size())
678    {
679      return false;
680    }
681
682    for (final Map.Entry<String,String[]> e : m1.entrySet())
683    {
684      final String[] v1 = e.getValue();
685      final String[] v2 = m2.get(e.getKey());
686      if (! StaticUtils.arraysEqualOrderIndependent(v1, v2))
687      {
688        return false;
689      }
690    }
691
692    return true;
693  }
694
695
696
697  /**
698   * Converts the provided collection of strings to an array.
699   *
700   * @param  c  The collection to convert to an array.  It may be {@code null}.
701   *
702   * @return  A string array if the provided collection is non-{@code null}, or
703   *          {@code null} if the provided collection is {@code null}.
704   */
705  static String[] toArray(final Collection<String> c)
706  {
707    if (c == null)
708    {
709      return null;
710    }
711
712    return c.toArray(StaticUtils.NO_STRINGS);
713  }
714
715
716
717  /**
718   * Retrieves a string representation of this schema element, in the format
719   * described in RFC 4512.
720   *
721   * @return  A string representation of this schema element, in the format
722   *          described in RFC 4512.
723   */
724  @Override()
725  public abstract String toString();
726}