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