001    /*
002     * Copyright 2007-2016 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2008-2016 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     */
021    package com.unboundid.ldap.sdk.schema;
022    
023    
024    
025    import java.io.Serializable;
026    import java.nio.ByteBuffer;
027    import java.util.ArrayList;
028    import java.util.Collection;
029    import java.util.Map;
030    
031    import com.unboundid.ldap.sdk.LDAPException;
032    import com.unboundid.ldap.sdk.ResultCode;
033    import com.unboundid.util.NotExtensible;
034    import com.unboundid.util.ThreadSafety;
035    import com.unboundid.util.ThreadSafetyLevel;
036    
037    import static com.unboundid.ldap.sdk.schema.SchemaMessages.*;
038    import static com.unboundid.util.Debug.*;
039    import 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)
050    public 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    }