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