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