001    /*
002     * CDDL HEADER START
003     *
004     * The contents of this file are subject to the terms of the
005     * Common Development and Distribution License, Version 1.0 only
006     * (the "License").  You may not use this file except in compliance
007     * with the License.
008     *
009     * You can obtain a copy of the license at
010     * trunk/opends/resource/legal-notices/OpenDS.LICENSE
011     * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
012     * See the License for the specific language governing permissions
013     * and limitations under the License.
014     *
015     * When distributing Covered Code, include this CDDL HEADER in each
016     * file and include the License file at
017     * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
018     * add the following below this CDDL HEADER, with the fields enclosed
019     * by brackets "[]" replaced with your own identifying information:
020     *      Portions Copyright [yyyy] [name of copyright owner]
021     *
022     * CDDL HEADER END
023     *
024     *
025     *      Copyright 2006-2008 Sun Microsystems, Inc.
026     */
027    package org.opends.server.schema;
028    import org.opends.messages.Message;
029    
030    
031    
032    import java.util.concurrent.ConcurrentHashMap;
033    import java.util.concurrent.CopyOnWriteArrayList;
034    
035    import org.opends.server.admin.std.server.AttributeSyntaxCfg;
036    import org.opends.server.api.ApproximateMatchingRule;
037    import org.opends.server.api.AttributeSyntax;
038    import org.opends.server.api.EqualityMatchingRule;
039    import org.opends.server.api.OrderingMatchingRule;
040    import org.opends.server.api.SubstringMatchingRule;
041    import org.opends.server.config.ConfigException;
042    import org.opends.server.core.DirectoryServer;
043    import org.opends.server.types.ByteString;
044    import org.opends.server.types.DirectoryException;
045    
046    
047    import org.opends.server.types.ResultCode;
048    
049    import org.opends.server.types.DebugLogLevel;
050    import static org.opends.server.loggers.ErrorLogger.*;
051    import static org.opends.server.loggers.debug.DebugLogger.*;
052    import org.opends.server.loggers.debug.DebugTracer;
053    import static org.opends.messages.SchemaMessages.*;
054    import org.opends.messages.MessageBuilder;
055    import static org.opends.server.schema.SchemaConstants.*;
056    import static org.opends.server.util.StaticUtils.*;
057    
058    
059    
060    /**
061     * This class implements the matching rule description syntax, which is used to
062     * hold matching rule definitions in the server schema.  The format of this
063     * syntax is defined in RFC 2252.
064     */
065    public class MatchingRuleSyntax
066           extends AttributeSyntax<AttributeSyntaxCfg>
067    {
068      /**
069       * The tracer object for the debug logger.
070       */
071      private static final DebugTracer TRACER = getTracer();
072    
073    
074    
075      // The default equality matching rule for this syntax.
076      private EqualityMatchingRule defaultEqualityMatchingRule;
077    
078      // The default ordering matching rule for this syntax.
079      private OrderingMatchingRule defaultOrderingMatchingRule;
080    
081      // The default substring matching rule for this syntax.
082      private SubstringMatchingRule defaultSubstringMatchingRule;
083    
084    
085    
086      /**
087       * Creates a new instance of this syntax.  Note that the only thing that
088       * should be done here is to invoke the default constructor for the
089       * superclass.  All initialization should be performed in the
090       * <CODE>initializeSyntax</CODE> method.
091       */
092      public MatchingRuleSyntax()
093      {
094        super();
095      }
096    
097    
098    
099      /**
100       * {@inheritDoc}
101       */
102      public void initializeSyntax(AttributeSyntaxCfg configuration)
103             throws ConfigException
104      {
105        defaultEqualityMatchingRule =
106             DirectoryServer.getEqualityMatchingRule(EMR_CASE_IGNORE_OID);
107        if (defaultEqualityMatchingRule == null)
108        {
109          logError(ERR_ATTR_SYNTAX_UNKNOWN_EQUALITY_MATCHING_RULE.get(
110              EMR_CASE_IGNORE_OID, SYNTAX_MATCHING_RULE_NAME));
111        }
112    
113        defaultOrderingMatchingRule =
114             DirectoryServer.getOrderingMatchingRule(OMR_CASE_IGNORE_OID);
115        if (defaultOrderingMatchingRule == null)
116        {
117          logError(ERR_ATTR_SYNTAX_UNKNOWN_ORDERING_MATCHING_RULE.get(
118              OMR_CASE_IGNORE_OID, SYNTAX_MATCHING_RULE_NAME));
119        }
120    
121        defaultSubstringMatchingRule =
122             DirectoryServer.getSubstringMatchingRule(SMR_CASE_IGNORE_OID);
123        if (defaultSubstringMatchingRule == null)
124        {
125          logError(ERR_ATTR_SYNTAX_UNKNOWN_SUBSTRING_MATCHING_RULE.get(
126              SMR_CASE_IGNORE_OID, SYNTAX_MATCHING_RULE_NAME));
127        }
128      }
129    
130    
131    
132      /**
133       * Retrieves the common name for this attribute syntax.
134       *
135       * @return  The common name for this attribute syntax.
136       */
137      public String getSyntaxName()
138      {
139        return SYNTAX_MATCHING_RULE_NAME;
140      }
141    
142    
143    
144      /**
145       * Retrieves the OID for this attribute syntax.
146       *
147       * @return  The OID for this attribute syntax.
148       */
149      public String getOID()
150      {
151        return SYNTAX_MATCHING_RULE_OID;
152      }
153    
154    
155    
156      /**
157       * Retrieves a description for this attribute syntax.
158       *
159       * @return  A description for this attribute syntax.
160       */
161      public String getDescription()
162      {
163        return SYNTAX_MATCHING_RULE_DESCRIPTION;
164      }
165    
166    
167    
168      /**
169       * Retrieves the default equality matching rule that will be used for
170       * attributes with this syntax.
171       *
172       * @return  The default equality matching rule that will be used for
173       *          attributes with this syntax, or <CODE>null</CODE> if equality
174       *          matches will not be allowed for this type by default.
175       */
176      public EqualityMatchingRule getEqualityMatchingRule()
177      {
178        return defaultEqualityMatchingRule;
179      }
180    
181    
182    
183      /**
184       * Retrieves the default ordering matching rule that will be used for
185       * attributes with this syntax.
186       *
187       * @return  The default ordering matching rule that will be used for
188       *          attributes with this syntax, or <CODE>null</CODE> if ordering
189       *          matches will not be allowed for this type by default.
190       */
191      public OrderingMatchingRule getOrderingMatchingRule()
192      {
193        return defaultOrderingMatchingRule;
194      }
195    
196    
197    
198      /**
199       * Retrieves the default substring matching rule that will be used for
200       * attributes with this syntax.
201       *
202       * @return  The default substring matching rule that will be used for
203       *          attributes with this syntax, or <CODE>null</CODE> if substring
204       *          matches will not be allowed for this type by default.
205       */
206      public SubstringMatchingRule getSubstringMatchingRule()
207      {
208        return defaultSubstringMatchingRule;
209      }
210    
211    
212    
213      /**
214       * Retrieves the default approximate matching rule that will be used for
215       * attributes with this syntax.
216       *
217       * @return  The default approximate matching rule that will be used for
218       *          attributes with this syntax, or <CODE>null</CODE> if approximate
219       *          matches will not be allowed for this type by default.
220       */
221      public ApproximateMatchingRule getApproximateMatchingRule()
222      {
223        // There is no approximate matching rule by default.
224        return null;
225      }
226    
227    
228    
229      /**
230       * Indicates whether the provided value is acceptable for use in an attribute
231       * with this syntax.  If it is not, then the reason may be appended to the
232       * provided buffer.
233       *
234       * @param  value          The value for which to make the determination.
235       * @param  invalidReason  The buffer to which the invalid reason should be
236       *                        appended.
237       *
238       * @return  <CODE>true</CODE> if the provided value is acceptable for use with
239       *          this syntax, or <CODE>false</CODE> if not.
240       */
241      public boolean valueIsAcceptable(ByteString value,
242                                       MessageBuilder invalidReason)
243      {
244        // Get string representations of the provided value using the provided form
245        // and with all lowercase characters.
246        String valueStr = value.stringValue();
247        String lowerStr = toLowerCase(valueStr);
248    
249    
250        // We'll do this a character at a time.  First, skip over any leading
251        // whitespace.
252        int pos    = 0;
253        int length = valueStr.length();
254        while ((pos < length) && (valueStr.charAt(pos) == ' '))
255        {
256          pos++;
257        }
258    
259        if (pos >= length)
260        {
261          // This means that the value was empty or contained only whitespace.  That
262          // is illegal.
263    
264          invalidReason.append(ERR_ATTR_SYNTAX_MR_EMPTY_VALUE.get());
265          return false;
266        }
267    
268    
269        // The next character must be an open parenthesis.  If it is not, then that
270        // is an error.
271        char c = valueStr.charAt(pos++);
272        if (c != '(')
273        {
274    
275          invalidReason.append(ERR_ATTR_SYNTAX_MR_EXPECTED_OPEN_PARENTHESIS.get(
276                  valueStr, (pos-1), String.valueOf(c)));
277          return false;
278        }
279    
280    
281        // Skip over any spaces immediately following the opening parenthesis.
282        while ((pos < length) && ((c = valueStr.charAt(pos)) == ' '))
283        {
284          pos++;
285        }
286    
287        if (pos >= length)
288        {
289          // This means that the end of the value was reached before we could find
290          // the OID.  Ths is illegal.
291    
292          invalidReason.append(ERR_ATTR_SYNTAX_MR_TRUNCATED_VALUE.get(valueStr));
293          return false;
294        }
295    
296    
297        // The next set of characters must be the OID.  Strictly speaking, this
298        // should only be a numeric OID, but we'll also allow for the
299        // "ocname-oid" case as well.  Look at the first character to figure out
300        // which we will be using.
301        int oidStartPos = pos;
302        if (isDigit(c))
303        {
304          // This must be a numeric OID.  In that case, we will accept only digits
305          // and periods, but not consecutive periods.
306          boolean lastWasPeriod = false;
307          while ((pos < length) && ((c = valueStr.charAt(pos++)) != ' '))
308          {
309            if (c == '.')
310            {
311              if (lastWasPeriod)
312              {
313    
314                invalidReason.append(
315                        ERR_ATTR_SYNTAX_MR_DOUBLE_PERIOD_IN_NUMERIC_OID.get(
316                                valueStr, (pos-1)));
317                return false;
318              }
319              else
320              {
321                lastWasPeriod = true;
322              }
323            }
324            else if (! isDigit(c))
325            {
326              // This must have been an illegal character.
327    
328              invalidReason.append(
329                      ERR_ATTR_SYNTAX_MR_ILLEGAL_CHAR_IN_NUMERIC_OID.get(
330                              valueStr, String.valueOf(c), (pos-1)));
331              return false;
332            }
333            else
334            {
335              lastWasPeriod = false;
336            }
337          }
338        }
339        else
340        {
341          // This must be a "fake" OID.  In this case, we will only accept
342          // alphabetic characters, numeric digits, and the hyphen.
343          while ((pos < length) && ((c = valueStr.charAt(pos++)) != ' '))
344          {
345            if (isAlpha(c) || isDigit(c) || (c == '-') ||
346                ((c == '_') && DirectoryServer.allowAttributeNameExceptions()))
347            {
348              // This is fine.  It is an acceptable character.
349            }
350            else
351            {
352              // This must have been an illegal character.
353    
354              invalidReason.append(
355                      ERR_ATTR_SYNTAX_MR_ILLEGAL_CHAR_IN_STRING_OID.get(
356                              valueStr, String.valueOf(c), (pos-1)));
357              return false;
358            }
359          }
360        }
361    
362    
363        // If we're at the end of the value, then it isn't a valid matching rule
364        // description.  Otherwise, parse out the OID.
365        String oid;
366        if (pos >= length)
367        {
368    
369          invalidReason.append(ERR_ATTR_SYNTAX_MR_TRUNCATED_VALUE.get(valueStr));
370          return false;
371        }
372        else
373        {
374          oid = lowerStr.substring(oidStartPos, (pos-1));
375        }
376    
377    
378        // Skip over the space(s) after the OID.
379        while ((pos < length) && ((c = valueStr.charAt(pos)) == ' '))
380        {
381          pos++;
382        }
383    
384        if (pos >= length)
385        {
386          // This means that the end of the value was reached before we could find
387          // the OID.  Ths is illegal.
388    
389          invalidReason.append(ERR_ATTR_SYNTAX_MR_TRUNCATED_VALUE.get(valueStr));
390          return false;
391        }
392    
393    
394        // At this point, we should have a pretty specific syntax that describes
395        // what may come next, but some of the components are optional and it would
396        // be pretty easy to put something in the wrong order, so we will be very
397        // flexible about what we can accept.  Just look at the next token, figure
398        // out what it is and how to treat what comes after it, then repeat until
399        // we get to the end of the value.  But before we start, set default values
400        // for everything else we might need to know.
401        ConcurrentHashMap<String,String> names =
402             new ConcurrentHashMap<String,String>();
403        AttributeSyntax syntax = null;
404        ConcurrentHashMap<String,CopyOnWriteArrayList<String>> extraProperties =
405             new ConcurrentHashMap<String,CopyOnWriteArrayList<String>>();
406    
407    
408        while (true)
409        {
410          StringBuilder tokenNameBuffer = new StringBuilder();
411    
412          try
413          {
414            pos = readTokenName(valueStr, tokenNameBuffer, pos);
415          }
416          catch (DirectoryException de)
417          {
418            if (debugEnabled())
419            {
420              TRACER.debugCaught(DebugLogLevel.ERROR, de);
421            }
422    
423            invalidReason.append(de.getMessageObject());
424            return false;
425          }
426    
427          String tokenName = tokenNameBuffer.toString();
428          String lowerTokenName = toLowerCase(tokenName);
429          if (tokenName.equals(")"))
430          {
431            // We must be at the end of the value.  If not, then that's a problem.
432            if (pos < length)
433            {
434    
435              invalidReason.append(
436                      ERR_ATTR_SYNTAX_MR_UNEXPECTED_CLOSE_PARENTHESIS.get(
437                              valueStr, (pos-1)));
438              return false;
439            }
440    
441            break;
442          }
443          else if (lowerTokenName.equals("name"))
444          {
445            // This specifies the set of names for the matching rule.  It may be a
446            // single name in single quotes, or it may be an open parenthesis
447            // followed by one or more names in single quotes separated by spaces.
448            c = valueStr.charAt(pos++);
449            if (c == '\'')
450            {
451              StringBuilder userBuffer  = new StringBuilder();
452              StringBuilder lowerBuffer = new StringBuilder();
453    
454              try
455              {
456                pos = readQuotedString(valueStr, lowerStr, userBuffer, lowerBuffer,
457                                       (pos-1));
458              }
459              catch (DirectoryException de)
460              {
461                if (debugEnabled())
462                {
463                  TRACER.debugCaught(DebugLogLevel.ERROR, de);
464                }
465    
466                invalidReason.append(de.getMessageObject());
467                return false;
468              }
469    
470              names.put(lowerBuffer.toString(), userBuffer.toString());
471            }
472            else if (c == '(')
473            {
474              StringBuilder userBuffer  = new StringBuilder();
475              StringBuilder lowerBuffer = new StringBuilder();
476    
477              try
478              {
479                pos = readQuotedString(valueStr, lowerStr, userBuffer, lowerBuffer,
480                                       pos);
481              }
482              catch (DirectoryException de)
483              {
484                if (debugEnabled())
485                {
486                  TRACER.debugCaught(DebugLogLevel.ERROR, de);
487                }
488    
489                invalidReason.append(de.getMessageObject());
490                return false;
491              }
492    
493              names.put(lowerBuffer.toString(), userBuffer.toString());
494    
495    
496              while (true)
497              {
498                if (valueStr.charAt(pos) == ')')
499                {
500                  // Skip over any spaces after the parenthesis.
501                  pos++;
502                  while ((pos < length) && ((c = valueStr.charAt(pos)) == ' '))
503                  {
504                    pos++;
505                  }
506    
507                  break;
508                }
509                else
510                {
511                  userBuffer  = new StringBuilder();
512                  lowerBuffer = new StringBuilder();
513    
514                  try
515                  {
516                    pos = readQuotedString(valueStr, lowerStr, userBuffer,
517                                           lowerBuffer, pos);
518                  }
519                  catch (DirectoryException de)
520                  {
521                    if (debugEnabled())
522                    {
523                      TRACER.debugCaught(DebugLogLevel.ERROR, de);
524                    }
525    
526                    invalidReason.append(de.getMessageObject());
527                    return false;
528                  }
529    
530                  names.put(lowerBuffer.toString(), userBuffer.toString());
531                }
532              }
533            }
534            else
535            {
536              // This is an illegal character.
537    
538              invalidReason.append(ERR_ATTR_SYNTAX_MR_ILLEGAL_CHAR.get(
539                      valueStr, String.valueOf(c), (pos-1)));
540              return false;
541            }
542          }
543          else if (lowerTokenName.equals("desc"))
544          {
545            // This specifies the description for the matching rule.  It is an
546            // arbitrary string of characters enclosed in single quotes.
547            StringBuilder descriptionBuffer = new StringBuilder();
548    
549            try
550            {
551              pos = readQuotedString(valueStr, descriptionBuffer, pos);
552            }
553            catch (DirectoryException de)
554            {
555              if (debugEnabled())
556              {
557                TRACER.debugCaught(DebugLogLevel.ERROR, de);
558              }
559    
560              invalidReason.append(de.getMessageObject());
561              return false;
562            }
563          }
564          else if (lowerTokenName.equals("obsolete"))
565          {
566            // This indicates whether the matching rule should be considered
567            // obsolete.  We do not need to do any more parsing for this token.
568          }
569          else if (lowerTokenName.equals("syntax"))
570          {
571            // This specifies the syntax for the matching rule.  Read the next
572            // token, which should be the OID of the associated syntax.
573            StringBuilder oidBuffer = new StringBuilder();
574    
575            try
576            {
577              pos = readWOID(lowerStr, oidBuffer, pos);
578            }
579            catch (DirectoryException de)
580            {
581              if (debugEnabled())
582              {
583                TRACER.debugCaught(DebugLogLevel.ERROR, de);
584              }
585    
586              invalidReason.append(de.getMessageObject());
587              return false;
588            }
589    
590            // See if the server recognizes that syntax.  If not, then log a
591            // warning.
592            syntax = DirectoryServer.getAttributeSyntax(oidBuffer.toString(),
593                                                        false);
594            if (syntax == null)
595            {
596              Message message = ERR_ATTR_SYNTAX_MR_UNKNOWN_SYNTAX.get(
597                  valueStr, oidBuffer.toString());
598              logError(message);
599    
600              syntax = DirectoryServer.getDefaultAttributeSyntax();
601            }
602          }
603          else
604          {
605            // This must be a non-standard property and it must be followed by
606            // either a single value in single quotes or an open parenthesis
607            // followed by one or more values in single quotes separated by spaces
608            // followed by a close parenthesis.
609            CopyOnWriteArrayList<String> valueList =
610                 new CopyOnWriteArrayList<String>();
611    
612            try
613            {
614              pos = readExtraParameterValues(valueStr, valueList, pos);
615            }
616            catch (DirectoryException de)
617            {
618              if (debugEnabled())
619              {
620                TRACER.debugCaught(DebugLogLevel.ERROR, de);
621              }
622    
623              invalidReason.append(de.getMessageObject());
624              return false;
625            }
626    
627            extraProperties.put(tokenName, valueList);
628          }
629        }
630    
631    
632        // Make sure that a syntax was specified.
633        if (syntax == null)
634        {
635    
636          invalidReason.append(ERR_ATTR_SYNTAX_MR_NO_SYNTAX.get(valueStr));
637          return false;
638        }
639    
640    
641        // If we've gotten here, then the value is acceptable.
642        return true;
643      }
644    
645    
646    
647      /**
648       * Reads the next token name from the matching rule definition, skipping over
649       * any leading or trailing spaces, and appends it to the provided buffer.
650       *
651       * @param  valueStr   The string representation of the matching rule
652       *                    definition.
653       * @param  tokenName  The buffer into which the token name will be written.
654       * @param  startPos   The position in the provided string at which to start
655       *                    reading the token name.
656       *
657       * @return  The position of the first character that is not part of the token
658       *          name or one of the trailing spaces after it.
659       *
660       * @throws  DirectoryException  If a problem is encountered while reading the
661       *                              token name.
662       */
663      private static int readTokenName(String valueStr, StringBuilder tokenName,
664                                       int startPos)
665              throws DirectoryException
666      {
667        // Skip over any spaces at the beginning of the value.
668        char c = '\u0000';
669        int  length = valueStr.length();
670        while ((startPos < length) && ((c = valueStr.charAt(startPos)) == ' '))
671        {
672          startPos++;
673        }
674    
675        if (startPos >= length)
676        {
677          Message message = ERR_ATTR_SYNTAX_MR_TRUNCATED_VALUE.get(valueStr);
678          throw new DirectoryException(
679                  ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
680        }
681    
682    
683        // Read until we find the next space.
684        while ((startPos < length) && ((c = valueStr.charAt(startPos++)) != ' '))
685        {
686          tokenName.append(c);
687        }
688    
689    
690        // Skip over any trailing spaces after the value.
691        while ((startPos < length) && ((c = valueStr.charAt(startPos)) == ' '))
692        {
693          startPos++;
694        }
695    
696    
697        // Return the position of the first non-space character after the token.
698        return startPos;
699      }
700    
701    
702    
703      /**
704       * Reads the value of a string enclosed in single quotes, skipping over the
705       * quotes and any leading or trailing spaces, and appending the string to the
706       * provided buffer.
707       *
708       * @param  valueStr     The user-provided representation of the matching rule
709       *                      definition.
710       * @param  valueBuffer  The buffer into which the user-provided representation
711       *                      of the value will be placed.
712       * @param  startPos     The position in the provided string at which to start
713       *                      reading the quoted string.
714       *
715       * @return  The position of the first character that is not part of the quoted
716       *          string or one of the trailing spaces after it.
717       *
718       * @throws  DirectoryException  If a problem is encountered while reading the
719       *                              quoted string.
720       */
721      private static int readQuotedString(String valueStr,
722                                          StringBuilder valueBuffer, int startPos)
723              throws DirectoryException
724      {
725        // Skip over any spaces at the beginning of the value.
726        char c = '\u0000';
727        int  length = valueStr.length();
728        while ((startPos < length) && ((c = valueStr.charAt(startPos)) == ' '))
729        {
730          startPos++;
731        }
732    
733        if (startPos >= length)
734        {
735          Message message = ERR_ATTR_SYNTAX_MR_TRUNCATED_VALUE.get(valueStr);
736          throw new DirectoryException(
737                  ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
738        }
739    
740    
741        // The next character must be a single quote.
742        if (c != '\'')
743        {
744          Message message =
745              ERR_ATTR_SYNTAX_MR_EXPECTED_QUOTE_AT_POS.get(
746                      valueStr, startPos, String.valueOf(c));
747          throw new DirectoryException(
748                  ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
749        }
750    
751    
752        // Read until we find the closing quote.
753        startPos++;
754        while ((startPos < length) && ((c = valueStr.charAt(startPos)) != '\''))
755        {
756          valueBuffer.append(c);
757          startPos++;
758        }
759    
760    
761        // Skip over any trailing spaces after the value.
762        startPos++;
763        while ((startPos < length) && ((c = valueStr.charAt(startPos)) == ' '))
764        {
765          startPos++;
766        }
767    
768    
769        // If we're at the end of the value, then that's illegal.
770        if (startPos >= length)
771        {
772          Message message = ERR_ATTR_SYNTAX_MR_TRUNCATED_VALUE.get(valueStr);
773          throw new DirectoryException(
774                  ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
775        }
776    
777    
778        // Return the position of the first non-space character after the token.
779        return startPos;
780      }
781    
782    
783    
784      /**
785       * Reads the value of a string enclosed in single quotes, skipping over the
786       * quotes and any leading or trailing spaces, and appending the string to the
787       * provided buffer.
788       *
789       * @param  valueStr     The user-provided representation of the matching rule
790       *                      definition.
791       * @param  lowerStr     The all-lowercase representation of the matching rule
792       *                      definition.
793       * @param  userBuffer   The buffer into which the user-provided representation
794       *                      of the value will be placed.
795       * @param  lowerBuffer  The buffer into which the all-lowercase representation
796       *                      of the value will be placed.
797       * @param  startPos     The position in the provided string at which to start
798       *                      reading the quoted string.
799       *
800       * @return  The position of the first character that is not part of the quoted
801       *          string or one of the trailing spaces after it.
802       *
803       * @throws  DirectoryException  If a problem is encountered while reading the
804       *                              quoted string.
805       */
806      private static int readQuotedString(String valueStr, String lowerStr,
807                                          StringBuilder userBuffer,
808                                          StringBuilder lowerBuffer, int startPos)
809              throws DirectoryException
810      {
811        // Skip over any spaces at the beginning of the value.
812        char c = '\u0000';
813        int  length = lowerStr.length();
814        while ((startPos < length) && ((c = lowerStr.charAt(startPos)) == ' '))
815        {
816          startPos++;
817        }
818    
819        if (startPos >= length)
820        {
821          Message message = ERR_ATTR_SYNTAX_MR_TRUNCATED_VALUE.get(lowerStr);
822          throw new DirectoryException(
823                  ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
824        }
825    
826    
827        // The next character must be a single quote.
828        if (c != '\'')
829        {
830          Message message =
831              ERR_ATTR_SYNTAX_MR_EXPECTED_QUOTE_AT_POS.get(
832                      valueStr, startPos, String.valueOf(c));
833          throw new DirectoryException(
834                  ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
835        }
836    
837    
838        // Read until we find the closing quote.
839        startPos++;
840        while ((startPos < length) && ((c = lowerStr.charAt(startPos)) != '\''))
841        {
842          lowerBuffer.append(c);
843          userBuffer.append(valueStr.charAt(startPos));
844          startPos++;
845        }
846    
847    
848        // Skip over any trailing spaces after the value.
849        startPos++;
850        while ((startPos < length) && ((c = lowerStr.charAt(startPos)) == ' '))
851        {
852          startPos++;
853        }
854    
855    
856        // If we're at the end of the value, then that's illegal.
857        if (startPos >= length)
858        {
859          Message message = ERR_ATTR_SYNTAX_MR_TRUNCATED_VALUE.get(lowerStr);
860          throw new DirectoryException(
861                  ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
862        }
863    
864    
865        // Return the position of the first non-space character after the token.
866        return startPos;
867      }
868    
869    
870    
871      /**
872       * Reads the attribute type/objectclass description or numeric OID from the
873       * provided string, skipping over any leading or trailing spaces, and
874       * appending the value to the provided buffer.
875       *
876       * @param  lowerStr    The string from which the name or OID is to be read.
877       * @param  woidBuffer  The buffer into which the name or OID should be
878       *                     appended.
879       * @param  startPos    The position at which to start reading.
880       *
881       * @return  The position of the first character after the name or OID that is
882       *          not a space.
883       *
884       * @throws  DirectoryException  If a problem is encountered while reading the
885       *                              name or OID.
886       */
887      private static int readWOID(String lowerStr, StringBuilder woidBuffer,
888                                  int startPos)
889              throws DirectoryException
890      {
891        // Skip over any spaces at the beginning of the value.
892        char c = '\u0000';
893        int  length = lowerStr.length();
894        while ((startPos < length) && ((c = lowerStr.charAt(startPos)) == ' '))
895        {
896          startPos++;
897        }
898    
899        if (startPos >= length)
900        {
901          Message message =
902              ERR_ATTR_SYNTAX_OBJECTCLASS_TRUNCATED_VALUE.get(lowerStr);
903          throw new DirectoryException(
904                  ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
905        }
906    
907    
908        // The next character must be either numeric (for an OID) or alphabetic (for
909        // an objectclass description).
910        if (isDigit(c))
911        {
912          // This must be a numeric OID.  In that case, we will accept only digits
913          // and periods, but not consecutive periods.
914          boolean lastWasPeriod = false;
915          while ((startPos < length) && ((c = lowerStr.charAt(startPos++)) != ' '))
916          {
917            if (c == '.')
918            {
919              if (lastWasPeriod)
920              {
921                Message message =
922                    ERR_ATTR_SYNTAX_OBJECTCLASS_DOUBLE_PERIOD_IN_NUMERIC_OID.
923                      get(lowerStr, (startPos-1));
924                throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
925                                             message);
926              }
927              else
928              {
929                woidBuffer.append(c);
930                lastWasPeriod = true;
931              }
932            }
933            else if (! isDigit(c))
934            {
935              // Technically, this must be an illegal character.  However, it is
936              // possible that someone just got sloppy and did not include a space
937              // between the name/OID and a closing parenthesis.  In that case,
938              // we'll assume it's the end of the value.  What's more, we'll have
939              // to prematurely return to nasty side effects from stripping off
940              // additional characters.
941              if (c == ')')
942              {
943                return (startPos-1);
944              }
945    
946              // This must have been an illegal character.
947              Message message =
948                      ERR_ATTR_SYNTAX_OBJECTCLASS_ILLEGAL_CHAR_IN_NUMERIC_OID.get(
949                              lowerStr, String.valueOf(c), (startPos - 1));
950              throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
951                                           message);
952            }
953            else
954            {
955              woidBuffer.append(c);
956              lastWasPeriod = false;
957            }
958          }
959        }
960        else if (isAlpha(c))
961        {
962          // This must be an objectclass description.  In this case, we will only
963          // accept alphabetic characters, numeric digits, and the hyphen.
964          while ((startPos < length) && ((c = lowerStr.charAt(startPos++)) != ' '))
965          {
966            if (isAlpha(c) || isDigit(c) || (c == '-') ||
967                ((c == '_') && DirectoryServer.allowAttributeNameExceptions()))
968            {
969              woidBuffer.append(c);
970            }
971            else
972            {
973              // Technically, this must be an illegal character.  However, it is
974              // possible that someone just got sloppy and did not include a space
975              // between the name/OID and a closing parenthesis.  In that case,
976              // we'll assume it's the end of the value.  What's more, we'll have
977              // to prematurely return to nasty side effects from stripping off
978              // additional characters.
979              if (c == ')')
980              {
981                return (startPos-1);
982              }
983    
984              // This must have been an illegal character.
985              Message message =
986                      ERR_ATTR_SYNTAX_OBJECTCLASS_ILLEGAL_CHAR_IN_STRING_OID.
987                              get(lowerStr, String.valueOf(c), (startPos - 1));
988              throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
989                                           message);
990            }
991          }
992        }
993        else
994        {
995          Message message =
996              ERR_ATTR_SYNTAX_OBJECTCLASS_ILLEGAL_CHAR.get(
997                      lowerStr, String.valueOf(c), startPos);
998          throw new DirectoryException(
999                  ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
1000        }
1001    
1002    
1003        // Skip over any trailing spaces after the value.
1004        while ((startPos < length) && ((c = lowerStr.charAt(startPos)) == ' '))
1005        {
1006          startPos++;
1007        }
1008    
1009    
1010        // If we're at the end of the value, then that's illegal.
1011        if (startPos >= length)
1012        {
1013          Message message =
1014              ERR_ATTR_SYNTAX_OBJECTCLASS_TRUNCATED_VALUE.get(lowerStr);
1015          throw new DirectoryException(
1016                  ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
1017        }
1018    
1019    
1020        // Return the position of the first non-space character after the token.
1021        return startPos;
1022      }
1023    
1024    
1025    
1026      /**
1027       * Reads the value for an "extra" parameter.  It will handle a single unquoted
1028       * word (which is technically illegal, but we'll allow it), a single quoted
1029       * string, or an open parenthesis followed by a space-delimited set of quoted
1030       * strings or unquoted words followed by a close parenthesis.
1031       *
1032       * @param  valueStr   The string containing the information to be read.
1033       * @param  valueList  The list of "extra" parameter values read so far.
1034       * @param  startPos   The position in the value string at which to start
1035       *                    reading.
1036       *
1037       * @return  The "extra" parameter value that was read.
1038       *
1039       * @throws  DirectoryException  If a problem occurs while attempting to read
1040       *                              the value.
1041       */
1042      private static int readExtraParameterValues(String valueStr,
1043                              CopyOnWriteArrayList<String> valueList, int startPos)
1044              throws DirectoryException
1045      {
1046        // Skip over any leading spaces.
1047        int length = valueStr.length();
1048        char c = valueStr.charAt(startPos++);
1049        while ((startPos < length) && (c == ' '))
1050        {
1051          c = valueStr.charAt(startPos++);
1052        }
1053    
1054        if (startPos >= length)
1055        {
1056          Message message = ERR_ATTR_SYNTAX_MR_TRUNCATED_VALUE.get(valueStr);
1057          throw new DirectoryException(
1058                  ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
1059        }
1060    
1061    
1062        // Look at the next character.  If it is a quote, then parse until the next
1063        // quote and end.  If it is an open parenthesis, then parse individual
1064        // values until the close parenthesis and end.  Otherwise, parse until the
1065        // next space and end.
1066        if (c == '\'')
1067        {
1068          // Parse until the closing quote.
1069          StringBuilder valueBuffer = new StringBuilder();
1070          while ((startPos < length) && ((c = valueStr.charAt(startPos++)) != '\''))
1071          {
1072            valueBuffer.append(c);
1073          }
1074    
1075          valueList.add(valueBuffer.toString());
1076        }
1077        else if (c == '(')
1078        {
1079          while (true)
1080          {
1081            // Skip over any leading spaces;
1082            startPos++;
1083            while ((startPos < length) && ((c = valueStr.charAt(startPos)) == ' '))
1084            {
1085              startPos++;
1086            }
1087    
1088            if (startPos >= length)
1089            {
1090              Message message = ERR_ATTR_SYNTAX_MR_TRUNCATED_VALUE.get(valueStr);
1091              throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
1092                                           message);
1093            }
1094    
1095    
1096            if (c == ')')
1097            {
1098              // This is the end of the list.
1099              break;
1100            }
1101            else if (c == '(')
1102            {
1103              // This is an illegal character.
1104              Message message =
1105                  ERR_ATTR_SYNTAX_MR_ILLEGAL_CHAR.get(
1106                          valueStr, String.valueOf(c), startPos);
1107              throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
1108                                           message);
1109            }
1110            else
1111            {
1112              // We'll recursively call this method to deal with this.
1113              startPos = readExtraParameterValues(valueStr, valueList, startPos);
1114            }
1115          }
1116        }
1117        else
1118        {
1119          // Parse until the next space.
1120          StringBuilder valueBuffer = new StringBuilder();
1121          while ((startPos < length) && ((c = valueStr.charAt(startPos++)) != ' '))
1122          {
1123            valueBuffer.append(c);
1124          }
1125    
1126          valueList.add(valueBuffer.toString());
1127        }
1128    
1129    
1130    
1131        // Skip over any trailing spaces.
1132        while ((startPos < length) && (valueStr.charAt(startPos) == ' '))
1133        {
1134          startPos++;
1135        }
1136    
1137        if (startPos >= length)
1138        {
1139          Message message = ERR_ATTR_SYNTAX_MR_TRUNCATED_VALUE.get(valueStr);
1140          throw new DirectoryException(
1141                  ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
1142        }
1143    
1144    
1145        return startPos;
1146      }
1147    }
1148