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 legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2006-2009 Sun Microsystems, Inc.
025 *      Portions Copyright 2011-2015 ForgeRock AS
026 */
027package org.opends.server.schema;
028
029import static org.opends.messages.SchemaMessages.*;
030import static org.opends.server.schema.SchemaConstants.*;
031import static org.opends.server.util.StaticUtils.*;
032
033import java.util.ArrayList;
034import java.util.HashMap;
035import java.util.LinkedHashMap;
036import java.util.LinkedList;
037import java.util.List;
038
039import org.forgerock.i18n.LocalizableMessage;
040import org.forgerock.i18n.slf4j.LocalizedLogger;
041import org.forgerock.opendj.ldap.ByteSequence;
042import org.forgerock.opendj.ldap.ByteString;
043import org.forgerock.opendj.ldap.ResultCode;
044import org.forgerock.opendj.ldap.schema.CoreSchema;
045import org.forgerock.opendj.ldap.schema.MatchingRule;
046import org.forgerock.opendj.ldap.schema.SchemaBuilder;
047import org.forgerock.opendj.ldap.schema.Syntax;
048import org.opends.server.admin.std.server.AttributeSyntaxCfg;
049import org.opends.server.api.AttributeSyntax;
050import org.opends.server.core.DirectoryServer;
051import org.opends.server.core.ServerContext;
052import org.opends.server.types.CommonSchemaElements;
053import org.opends.server.types.DirectoryException;
054import org.opends.server.types.LDAPSyntaxDescription;
055import org.opends.server.types.Schema;
056
057/**
058 * This class defines the LDAP syntax description syntax, which is used to
059 * hold attribute syntax definitions in the server schema.  The format of this
060 * syntax is defined in RFC 2252.
061 */
062public class LDAPSyntaxDescriptionSyntax
063       extends AttributeSyntax<AttributeSyntaxCfg>
064{
065  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
066
067  /** The default equality matching rule for this syntax. */
068  private MatchingRule defaultEqualityMatchingRule;
069
070  /** The default ordering matching rule for this syntax. */
071  private MatchingRule defaultOrderingMatchingRule;
072
073  /** The default substring matching rule for this syntax. */
074  private MatchingRule defaultSubstringMatchingRule;
075
076  /**
077   * Creates a new instance of this syntax.  Note that the only thing that
078   * should be done here is to invoke the default constructor for the
079   * superclass.  All initialization should be performed in the
080   * <CODE>initializeSyntax</CODE> method.
081   */
082  public LDAPSyntaxDescriptionSyntax()
083  {
084    super();
085  }
086
087  /** {@inheritDoc} */
088  @Override
089  public Syntax getSDKSyntax(org.forgerock.opendj.ldap.schema.Schema schema)
090  {
091    return schema.getSyntax(SchemaConstants.SYNTAX_LDAP_SYNTAX_OID);
092  }
093
094  /**
095   * Retrieves the common name for this attribute syntax.
096   *
097   * @return  The common name for this attribute syntax.
098   */
099  @Override
100  public String getName()
101  {
102    return SYNTAX_LDAP_SYNTAX_NAME;
103  }
104
105  /**
106   * Retrieves the OID for this attribute syntax.
107   *
108   * @return  The OID for this attribute syntax.
109   */
110  @Override
111  public String getOID()
112  {
113    return SYNTAX_LDAP_SYNTAX_OID;
114  }
115
116  /**
117   * Retrieves a description for this attribute syntax.
118   *
119   * @return  A description for this attribute syntax.
120   */
121  @Override
122  public String getDescription()
123  {
124    return SYNTAX_LDAP_SYNTAX_DESCRIPTION;
125  }
126
127  /**
128   * Retrieves the default equality matching rule that will be used for
129   * attributes with this syntax.
130   *
131   * @return  The default equality matching rule that will be used for
132   *          attributes with this syntax, or <CODE>null</CODE> if equality
133   *          matches will not be allowed for this type by default.
134   */
135  @Override
136  public MatchingRule getEqualityMatchingRule()
137  {
138    return defaultEqualityMatchingRule;
139  }
140
141  /**
142   * Retrieves the default ordering matching rule that will be used for
143   * attributes with this syntax.
144   *
145   * @return  The default ordering matching rule that will be used for
146   *          attributes with this syntax, or <CODE>null</CODE> if ordering
147   *          matches will not be allowed for this type by default.
148   */
149  @Override
150  public MatchingRule getOrderingMatchingRule()
151  {
152    return defaultOrderingMatchingRule;
153  }
154
155  /**
156   * Retrieves the default substring matching rule that will be used for
157   * attributes with this syntax.
158   *
159   * @return  The default substring matching rule that will be used for
160   *          attributes with this syntax, or <CODE>null</CODE> if substring
161   *          matches will not be allowed for this type by default.
162   */
163  @Override
164  public MatchingRule getSubstringMatchingRule()
165  {
166    return defaultSubstringMatchingRule;
167  }
168
169  /**
170   * Retrieves the default approximate matching rule that will be used for
171   * attributes with this syntax.
172   *
173   * @return  The default approximate matching rule that will be used for
174   *          attributes with this syntax, or <CODE>null</CODE> if approximate
175   *          matches will not be allowed for this type by default.
176   */
177  @Override
178  public MatchingRule getApproximateMatchingRule()
179  {
180    // There is no approximate matching rule by default.
181    return null;
182  }
183
184  /**
185   * Decodes the contents of the provided byte sequence as an ldap syntax
186   * definition according to the rules of this syntax.  Note that the provided
187   * byte sequence value does not need to be normalized (and in fact, it should
188   * not be in order to allow the desired capitalization to be preserved).
189   *
190   * @param  value                 The byte sequence containing the value
191   *                               to decode (it does not need to be
192   *                               normalized).
193   * @param serverContext
194   *            The server context.
195   * @param  schema                The schema to use to resolve references to
196   *                               other schema elements.
197   * @param  allowUnknownElements  Indicates whether to allow values that are
198   *                               not defined in the server schema. This
199   *                               should only be true when called by
200   *                               {@code valueIsAcceptable}.
201   *                               Not used for LDAP Syntaxes
202   * @param forDelete
203   *            {@code true} if used for deletion.
204   *
205   * @return  The decoded ldapsyntax definition.
206   *
207   * @throws  DirectoryException  If the provided value cannot be decoded as an
208   *                              ldapsyntax definition.
209   */
210  public static LDAPSyntaxDescription decodeLDAPSyntax(ByteSequence value, ServerContext serverContext,
211          Schema schema, boolean allowUnknownElements, boolean forDelete) throws DirectoryException
212  {
213    // Get string representations of the provided value using the provided form.
214    String valueStr = value.toString();
215
216    // We'll do this a character at a time.  First, skip over any leading
217    // whitespace.
218    int pos    = 0;
219    int length = valueStr.length();
220    while (pos < length && valueStr.charAt(pos) == ' ')
221    {
222      pos++;
223    }
224
225    if (pos >= length)
226    {
227      // This means that the value was empty or contained only whitespace.  That
228      // is illegal.
229
230      LocalizableMessage message = ERR_ATTR_SYNTAX_LDAPSYNTAX_EMPTY_VALUE.get();
231      throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
232              message);
233    }
234
235
236    // The next character must be an open parenthesis.  If it is not, then that
237    // is an error.
238    char c = valueStr.charAt(pos++);
239    if (c != '(')
240    {
241      LocalizableMessage message =
242              ERR_ATTR_SYNTAX_LDAPSYNTAX_EXPECTED_OPEN_PARENTHESIS.get(valueStr, pos-1, c);
243      throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
244              message);
245    }
246
247
248    // Skip over any spaces immediately following the opening parenthesis.
249    while (pos < length && ((c = valueStr.charAt(pos)) == ' '))
250    {
251      pos++;
252    }
253
254    if (pos >= length)
255    {
256      // This means that the end of the value was reached before we could find
257      // the OID.  Ths is illegal.
258      LocalizableMessage message = ERR_ATTR_SYNTAX_LDAPSYNTAX_TRUNCATED_VALUE.get(
259              valueStr);
260      throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
261              message);
262    }
263
264    int oidStartPos = pos;
265    if (isDigit(c))
266    {
267      // This must be a numeric OID.  In that case, we will accept only digits
268      // and periods, but not consecutive periods.
269      boolean lastWasPeriod = false;
270      while (pos < length && ((c = valueStr.charAt(pos)) != ' ')
271              && (c = valueStr.charAt(pos)) != ')')
272      {
273        if (c == '.')
274        {
275          if (lastWasPeriod)
276          {
277            LocalizableMessage message =
278              ERR_ATTR_SYNTAX_LDAPSYNTAX_DOUBLE_PERIOD_IN_NUMERIC_OID.
279                  get(valueStr, pos-1);
280            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
281                                         message);
282          }
283          else
284          {
285            lastWasPeriod = true;
286          }
287        }
288        else if (! isDigit(c))
289        {
290          // This must have been an illegal character.
291          LocalizableMessage message =
292              ERR_ATTR_SYNTAX_LDAPSYNTAX_ILLEGAL_CHAR_IN_NUMERIC_OID.
293                get(valueStr, c, pos-1);
294          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
295                                       message);
296        }
297        else
298        {
299          lastWasPeriod = false;
300        }
301        pos++;
302      }
303    }
304    else
305    {
306      // This must be a "fake" OID.  In this case, we will only accept
307      // alphabetic characters, numeric digits, and the hyphen.
308      while (pos < length && ((c = valueStr.charAt(pos)) != ' ')
309              && (c=valueStr.charAt(pos))!=')')
310      {
311        if (isAlpha(c) || isDigit(c) || c == '-' ||
312            (c == '_' && DirectoryServer.allowAttributeNameExceptions()))
313        {
314          // This is fine.  It is an acceptable character.
315          pos++;
316        }
317        else
318        {
319          // This must have been an illegal character.
320          LocalizableMessage message =
321                  ERR_ATTR_SYNTAX_LDAPSYNTAX_ILLEGAL_CHAR_IN_STRING_OID.
322              get(valueStr, c, pos-1);
323          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
324                                       message);
325        }
326      }
327    }
328
329    // If we're at the end of the value, then it isn't a valid attribute type
330    // description.  Otherwise, parse out the OID.
331    String oid;
332    if (pos >= length)
333    {
334      LocalizableMessage message = ERR_ATTR_SYNTAX_LDAPSYNTAX_TRUNCATED_VALUE.get(
335              valueStr);
336      throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
337              message);
338    }
339    oid = toLowerCase(valueStr.substring(oidStartPos, pos));
340
341    // Skip over the space(s) after the OID.
342    while (pos < length && ((c = valueStr.charAt(pos)) == ' '))
343    {
344      pos++;
345    }
346
347    if (pos >= length)
348    {
349      // This means that the end of the value was reached before we could find
350      // the OID.  Ths is illegal.
351      LocalizableMessage message = ERR_ATTR_SYNTAX_LDAPSYNTAX_TRUNCATED_VALUE.get(
352              valueStr);
353      throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
354              message);
355    }
356
357    // At this point, we should have a pretty specific syntax that describes
358    // what may come next, but some of the components are optional and it would
359    // be pretty easy to put something in the wrong order, so we will be very
360    // flexible about what we can accept.  Just look at the next token, figure
361    // out what it is and how to treat what comes after it, then repeat until
362    // we get to the end of the value.  But before we start, set default values
363    // for everything else we might need to know.
364    String description = null;
365    Syntax syntax = null;
366    HashMap<String,List<String>> extraProperties = new LinkedHashMap<>();
367    boolean hasXSyntaxToken = false;
368
369    while (true)
370    {
371      StringBuilder tokenNameBuffer = new StringBuilder();
372      pos = readTokenName(valueStr, tokenNameBuffer, pos);
373      String tokenName = tokenNameBuffer.toString();
374      String lowerTokenName = toLowerCase(tokenName);
375      if (tokenName.equals(")"))
376      {
377        // We must be at the end of the value.  If not, then that's a problem.
378        if (pos < length)
379        {
380          LocalizableMessage message =
381            ERR_ATTR_SYNTAX_LDAPSYNTAX_UNEXPECTED_CLOSE_PARENTHESIS.get(valueStr, pos-1);
382          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
383        }
384
385        break;
386      }
387      else if (lowerTokenName.equals("desc"))
388      {
389        // This specifies the description for the attribute type.  It is an
390        // arbitrary string of characters enclosed in single quotes.
391        StringBuilder descriptionBuffer = new StringBuilder();
392        pos = readQuotedString(valueStr, descriptionBuffer, pos);
393        description = descriptionBuffer.toString();
394      }
395      else
396      {
397        SchemaBuilder schemaBuilder = serverContext != null ?
398            serverContext.getSchemaUpdater().getSchemaBuilder() : new SchemaBuilder(CoreSchema.getInstance());
399
400        if (lowerTokenName.equals("x-subst"))
401        {
402          if (hasXSyntaxToken)
403          {
404            // We've already seen syntax extension. More than 1 is not allowed
405            LocalizableMessage message =
406                ERR_ATTR_SYNTAX_LDAPSYNTAX_TOO_MANY_EXTENSIONS.get(valueStr);
407            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
408                                         message);
409          }
410          hasXSyntaxToken = true;
411          StringBuilder woidBuffer = new StringBuilder();
412          pos = readQuotedString(valueStr, woidBuffer, pos);
413          String syntaxOID = toLowerCase(woidBuffer.toString());
414          Syntax subSyntax = schema.getSyntax(syntaxOID);
415          if (subSyntax == null)
416          {
417            LocalizableMessage message = ERR_ATTR_SYNTAX_LDAPSYNTAX_UNKNOWN_SYNTAX.get(oid, syntaxOID);
418            throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
419          }
420          syntax = forDelete ? schema.getSyntax(oid) : schemaBuilder.buildSyntax(oid)
421              .extraProperties("x-subst", syntaxOID)
422              .addToSchema().toSchema().getSyntax(oid);
423        }
424
425        else if(lowerTokenName.equals("x-pattern"))
426        {
427          if (hasXSyntaxToken)
428          {
429            // We've already seen syntax extension. More than 1 is not allowed
430            LocalizableMessage message =
431                ERR_ATTR_SYNTAX_LDAPSYNTAX_TOO_MANY_EXTENSIONS.get(valueStr);
432            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
433                                         message);
434          }
435          hasXSyntaxToken = true;
436          StringBuilder regexBuffer = new StringBuilder();
437          pos = readQuotedString(valueStr, regexBuffer, pos);
438          String regex = regexBuffer.toString().trim();
439          if(regex.length() == 0)
440          {
441            LocalizableMessage message = WARN_ATTR_SYNTAX_LDAPSYNTAX_REGEX_NO_PATTERN.get(
442                 valueStr);
443            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
444                                         message);
445          }
446
447          try
448          {
449            syntax = forDelete ? schema.getSyntax(oid) : schemaBuilder.buildSyntax(oid)
450                .extraProperties("x-pattern", regex)
451                .addToSchema().toSchema().getSyntax(oid);
452          }
453          catch(Exception e)
454          {
455            LocalizableMessage message =
456                WARN_ATTR_SYNTAX_LDAPSYNTAX_REGEX_INVALID_PATTERN.get
457                    (valueStr,regex);
458            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
459                                         message);
460          }
461        }
462        else if(lowerTokenName.equals("x-enum"))
463        {
464          if (hasXSyntaxToken)
465          {
466            // We've already seen syntax extension. More than 1 is not allowed
467            LocalizableMessage message =
468                ERR_ATTR_SYNTAX_LDAPSYNTAX_TOO_MANY_EXTENSIONS.get(valueStr);
469            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
470                                         message);
471          }
472          hasXSyntaxToken = true;
473          LinkedList<String> values = new LinkedList<>();
474          pos = readExtraParameterValues(valueStr, values, pos);
475
476          if (values.isEmpty())
477          {
478            LocalizableMessage message =
479                ERR_ATTR_SYNTAX_LDAPSYNTAX_ENUM_NO_VALUES.get(valueStr);
480            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
481                                         message);
482          }
483          // Parse all enum values, check for uniqueness
484          List<String> entries = new LinkedList<>();
485          for (String v : values)
486          {
487            ByteString entry = ByteString.valueOfUtf8(v);
488            if (entries.contains(entry))
489            {
490              throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
491                  WARN_ATTR_SYNTAX_LDAPSYNTAX_ENUM_DUPLICATE_VALUE.get(
492                      valueStr, entry,pos));
493            }
494            entries.add(v);
495          }
496
497          syntax = forDelete ? schema.getSyntax(oid) : schemaBuilder
498              .addEnumerationSyntax(oid, description, true, entries.toArray(new String[0]))
499              .toSchema().getSyntax(oid);
500        }
501        else if (tokenName.matches("X\\-[_\\p{Alpha}-]+"))
502        {
503          // This must be a non-standard property and it must be followed by
504          // either a single value in single quotes or an open parenthesis
505          // followed by one or more values in single quotes separated by spaces
506          // followed by a close parenthesis.
507          List<String> valueList = new ArrayList<>();
508          pos = readExtraParameterValues(valueStr, valueList, pos);
509          extraProperties.put(tokenName, valueList);
510        }
511        else
512        {
513          // Unknown Token
514          LocalizableMessage message = ERR_ATTR_SYNTAX_LDAPSYNTAX_UNKNOWN_EXT.get(
515              valueStr, tokenName, pos);
516          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
517                message);
518        }
519      }
520    }
521    if (syntax == null)
522    {
523      // TODO : add localized message
524      throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
525          LocalizableMessage.raw("no LDAP syntax for:" + value));
526    }
527
528    CommonSchemaElements.checkSafeProperties(extraProperties);
529
530    //Since we reached here it means everything is OK.
531    return new LDAPSyntaxDescription(valueStr, syntax, extraProperties);
532  }
533
534  /**
535   * Reads the next token name from the attribute syntax definition, skipping
536   * over any leading or trailing spaces, and appends it to the provided buffer.
537   *
538   * @param  valueStr   The string representation of the attribute syntax
539   *                    definition.
540   * @param  tokenName  The buffer into which the token name will be written.
541   * @param  startPos   The position in the provided string at which to start
542   *                    reading the token name.
543   *
544   * @return  The position of the first character that is not part of the token
545   *          name or one of the trailing spaces after it.
546   *
547   * @throws  DirectoryException  If a problem is encountered while reading the
548   *                              token name.
549   */
550  private static int readTokenName(String valueStr, StringBuilder tokenName,
551                                   int startPos)
552          throws DirectoryException
553  {
554    // Skip over any spaces at the beginning of the value.
555    char c = '\u0000';
556    int  length = valueStr.length();
557    while (startPos < length && ((c = valueStr.charAt(startPos)) == ' '))
558    {
559      startPos++;
560    }
561
562    if (startPos >= length)
563    {
564      LocalizableMessage message =
565          ERR_ATTR_SYNTAX_LDAPSYNTAX_TRUNCATED_VALUE.get(valueStr);
566      throw new DirectoryException(
567              ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
568    }
569
570
571    // Read until we find the next space.
572    while (startPos < length && ((c = valueStr.charAt(startPos++)) != ' '))
573    {
574      tokenName.append(c);
575    }
576
577
578    // Skip over any trailing spaces after the value.
579    while (startPos < length && ((c = valueStr.charAt(startPos)) == ' '))
580    {
581      startPos++;
582    }
583
584
585    // Return the position of the first non-space character after the token.
586    return startPos;
587  }
588
589  /**
590   * Reads the value of a string enclosed in single quotes, skipping over the
591   * quotes and any leading or trailing spaces, and appending the string to the
592   * provided buffer.
593   *
594   * @param  valueStr     The user-provided representation of the attribute type
595   *                      definition.
596   * @param  valueBuffer  The buffer into which the user-provided representation
597   *                      of the value will be placed.
598   * @param  startPos     The position in the provided string at which to start
599   *                      reading the quoted string.
600   *
601   * @return  The position of the first character that is not part of the quoted
602   *          string or one of the trailing spaces after it.
603   *
604   * @throws  DirectoryException  If a problem is encountered while reading the
605   *                              quoted string.
606   */
607  private static int readQuotedString(String valueStr,
608                                      StringBuilder valueBuffer, int startPos)
609          throws DirectoryException
610  {
611    // Skip over any spaces at the beginning of the value.
612    char c = '\u0000';
613    int  length = valueStr.length();
614    while (startPos < length && ((c = valueStr.charAt(startPos)) == ' '))
615    {
616      startPos++;
617    }
618
619    if (startPos >= length)
620    {
621      LocalizableMessage message =
622          ERR_ATTR_SYNTAX_LDAPSYNTAX_TRUNCATED_VALUE.get(valueStr);
623      throw new DirectoryException(
624              ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
625    }
626
627
628    // The next character must be a single quote.
629    if (c != '\'')
630    {
631      LocalizableMessage message = ERR_ATTR_SYNTAX_LDAPSYNTAX_EXPECTED_QUOTE_AT_POS.get(valueStr, startPos, c);
632      throw new DirectoryException(
633              ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
634    }
635
636
637    // Read until we find the closing quote.
638    startPos++;
639    while (startPos < length && ((c = valueStr.charAt(startPos)) != '\''))
640    {
641      valueBuffer.append(c);
642      startPos++;
643    }
644
645
646    // Skip over any trailing spaces after the value.
647    startPos++;
648    while (startPos < length && ((c = valueStr.charAt(startPos)) == ' '))
649    {
650      startPos++;
651    }
652
653
654    // If we're at the end of the value, then that's illegal.
655    if (startPos >= length)
656    {
657      LocalizableMessage message =
658          ERR_ATTR_SYNTAX_LDAPSYNTAX_TRUNCATED_VALUE.get(valueStr);
659      throw new DirectoryException(
660              ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
661    }
662
663
664    // Return the position of the first non-space character after the token.
665    return startPos;
666  }
667
668
669  /**
670   * Reads the value for an "extra" parameter.  It will handle a single unquoted
671   * word (which is technically illegal, but we'll allow it), a single quoted
672   * string, or an open parenthesis followed by a space-delimited set of quoted
673   * strings or unquoted words followed by a close parenthesis.
674   *
675   * @param  valueStr   The string containing the information to be read.
676   * @param  valueList  The list of "extra" parameter values read so far.
677   * @param  startPos   The position in the value string at which to start
678   *                    reading.
679   *
680   * @return  The "extra" parameter value that was read.
681   *
682   * @throws  DirectoryException  If a problem occurs while attempting to read
683   *                              the value.
684   */
685  private static int readExtraParameterValues(String valueStr,
686                          List<String> valueList, int startPos)
687          throws DirectoryException
688  {
689    // Skip over any leading spaces.
690    int length = valueStr.length();
691    char c = '\u0000';
692    while (startPos < length && ((c = valueStr.charAt(startPos)) == ' '))
693    {
694      startPos++;
695    }
696
697    if (startPos >= length)
698    {
699      LocalizableMessage message =
700          ERR_ATTR_SYNTAX_LDAPSYNTAX_TRUNCATED_VALUE.get(valueStr);
701      throw new DirectoryException(
702              ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
703    }
704
705
706    // Look at the next character.  If it is a quote, then parse until the next
707    // quote and end.  If it is an open parenthesis, then parse individual
708    // values until the close parenthesis and end.  Otherwise, parse until the
709    // next space and end.
710    if (c == '\'')
711    {
712      // Parse until the closing quote.
713      StringBuilder valueBuffer = new StringBuilder();
714      startPos++;
715      while (startPos < length && ((c = valueStr.charAt(startPos)) != '\''))
716      {
717        valueBuffer.append(c);
718        startPos++;
719      }
720      startPos++;
721      valueList.add(valueBuffer.toString());
722    }
723    else if (c == '(')
724    {
725      startPos++;
726      // We're expecting a list of values. Quoted, space separated.
727      while (true)
728      {
729        // Skip over any leading spaces;
730        while (startPos < length && ((c = valueStr.charAt(startPos)) == ' '))
731        {
732          startPos++;
733        }
734
735        if (startPos >= length)
736        {
737          LocalizableMessage message =
738              ERR_ATTR_SYNTAX_LDAPSYNTAX_TRUNCATED_VALUE.get(valueStr);
739          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
740                                       message);
741        }
742
743        if (c == ')')
744        {
745          // This is the end of the list.
746          startPos++;
747          break;
748        }
749        else if (c == '(')
750        {
751          // This is an illegal character.
752          LocalizableMessage message =
753              ERR_ATTR_SYNTAX_LDAPSYNTAX_EXTENSION_INVALID_CHARACTER.get(
754                      valueStr, startPos);
755          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
756                                       message);
757        }
758        else if (c == '\'')
759        {
760          // We have a quoted string
761          StringBuilder valueBuffer = new StringBuilder();
762          startPos++;
763          while (startPos < length && ((c = valueStr.charAt(startPos)) != '\''))
764          {
765            valueBuffer.append(c);
766            startPos++;
767          }
768
769          valueList.add(valueBuffer.toString());
770          startPos++;
771        }
772        else
773        {
774          //Consider unquoted string
775          StringBuilder valueBuffer = new StringBuilder();
776          while (startPos < length && ((c = valueStr.charAt(startPos)) != ' '))
777          {
778            valueBuffer.append(c);
779            startPos++;
780          }
781
782          valueList.add(valueBuffer.toString());
783        }
784
785        if (startPos >= length)
786        {
787          LocalizableMessage message =
788              ERR_ATTR_SYNTAX_LDAPSYNTAX_TRUNCATED_VALUE.get(valueStr);
789          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
790                                       message);
791        }
792      }
793    }
794    else
795    {
796      // Parse until the next space.
797      StringBuilder valueBuffer = new StringBuilder();
798      while (startPos < length && ((c = valueStr.charAt(startPos)) != ' '))
799      {
800        valueBuffer.append(c);
801        startPos++;
802      }
803
804      valueList.add(valueBuffer.toString());
805    }
806
807    // Skip over any trailing spaces.
808    while (startPos < length && valueStr.charAt(startPos) == ' ')
809    {
810      startPos++;
811    }
812
813    if (startPos >= length)
814    {
815      LocalizableMessage message =
816          ERR_ATTR_SYNTAX_LDAPSYNTAX_TRUNCATED_VALUE.get(valueStr);
817      throw new DirectoryException(
818              ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
819    }
820
821    return startPos;
822  }
823}