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