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