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