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