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