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