001    /*
002     * Copyright 2007-2014 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2008-2014 UnboundID Corp.
007     *
008     * This program is free software; you can redistribute it and/or modify
009     * it under the terms of the GNU General Public License (GPLv2 only)
010     * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011     * as published by the Free Software Foundation.
012     *
013     * This program is distributed in the hope that it will be useful,
014     * but WITHOUT ANY WARRANTY; without even the implied warranty of
015     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016     * GNU General Public License for more details.
017     *
018     * You should have received a copy of the GNU General Public License
019     * along with this program; if not, see <http://www.gnu.org/licenses>.
020     */
021    package com.unboundid.ldap.sdk.schema;
022    
023    
024    
025    import java.util.ArrayList;
026    import java.util.Collections;
027    import java.util.HashSet;
028    import java.util.Map;
029    import java.util.LinkedHashMap;
030    import java.util.LinkedHashSet;
031    import java.util.Set;
032    
033    import com.unboundid.ldap.sdk.LDAPException;
034    import com.unboundid.ldap.sdk.ResultCode;
035    
036    import static com.unboundid.ldap.sdk.schema.SchemaMessages.*;
037    import static com.unboundid.util.StaticUtils.*;
038    import static com.unboundid.util.Validator.*;
039    
040    
041    
042    /**
043     * This class provides a data structure that describes an LDAP object class
044     * schema element.
045     */
046    public final class ObjectClassDefinition
047           extends SchemaElement
048    {
049      /**
050       * The serial version UID for this serializable class.
051       */
052      private static final long serialVersionUID = -3024333376249332728L;
053    
054    
055    
056      // Indicates whether this object class is declared obsolete.
057      private final boolean isObsolete;
058    
059      // The set of extensions for this object class.
060      private final Map<String,String[]> extensions;
061    
062      // The object class type for this object class.
063      private final ObjectClassType objectClassType;
064    
065      // The description for this object class.
066      private final String description;
067    
068      // The string representation of this object class.
069      private final String objectClassString;
070    
071      // The OID for this object class.
072      private final String oid;
073    
074      // The set of names for this object class.
075      private final String[] names;
076    
077      // The names/OIDs of the optional attributes.
078      private final String[] optionalAttributes;
079    
080      // The names/OIDs of the required attributes.
081      private final String[] requiredAttributes;
082    
083      // The set of superior object class names/OIDs.
084      private final String[] superiorClasses;
085    
086    
087    
088      /**
089       * Creates a new object class from the provided string representation.
090       *
091       * @param  s  The string representation of the object class to create, using
092       *            the syntax described in RFC 4512 section 4.1.1.  It must not be
093       *            {@code null}.
094       *
095       * @throws  LDAPException  If the provided string cannot be decoded as an
096       *                         object class definition.
097       */
098      public ObjectClassDefinition(final String s)
099             throws LDAPException
100      {
101        ensureNotNull(s);
102    
103        objectClassString = s.trim();
104    
105        // The first character must be an opening parenthesis.
106        final int length = objectClassString.length();
107        if (length == 0)
108        {
109          throw new LDAPException(ResultCode.DECODING_ERROR,
110                                  ERR_OC_DECODE_EMPTY.get());
111        }
112        else if (objectClassString.charAt(0) != '(')
113        {
114          throw new LDAPException(ResultCode.DECODING_ERROR,
115                                  ERR_OC_DECODE_NO_OPENING_PAREN.get(
116                                       objectClassString));
117        }
118    
119    
120        // Skip over any spaces until we reach the start of the OID, then read the
121        // OID until we find the next space.
122        int pos = skipSpaces(objectClassString, 1, length);
123    
124        StringBuilder buffer = new StringBuilder();
125        pos = readOID(objectClassString, pos, length, buffer);
126        oid = buffer.toString();
127    
128    
129        // Technically, object class elements are supposed to appear in a specific
130        // order, but we'll be lenient and allow remaining elements to come in any
131        // order.
132        final ArrayList<String>    nameList = new ArrayList<String>(1);
133        final ArrayList<String>    supList  = new ArrayList<String>(1);
134        final ArrayList<String>    reqAttrs = new ArrayList<String>();
135        final ArrayList<String>    optAttrs = new ArrayList<String>();
136        final Map<String,String[]> exts     = new LinkedHashMap<String,String[]>();
137        Boolean                    obsolete = null;
138        ObjectClassType            ocType   = null;
139        String                     descr    = null;
140    
141        while (true)
142        {
143          // Skip over any spaces until we find the next element.
144          pos = skipSpaces(objectClassString, pos, length);
145    
146          // Read until we find the next space or the end of the string.  Use that
147          // token to figure out what to do next.
148          final int tokenStartPos = pos;
149          while ((pos < length) && (objectClassString.charAt(pos) != ' '))
150          {
151            pos++;
152          }
153    
154          // It's possible that the token could be smashed right up against the
155          // closing parenthesis.  If that's the case, then extract just the token
156          // and handle the closing parenthesis the next time through.
157          String token = objectClassString.substring(tokenStartPos, pos);
158          if ((token.length() > 1) && (token.endsWith(")")))
159          {
160            token = token.substring(0, token.length() - 1);
161            pos--;
162          }
163    
164          final String lowerToken = toLowerCase(token);
165          if (lowerToken.equals(")"))
166          {
167            // This indicates that we're at the end of the value.  There should not
168            // be any more closing characters.
169            if (pos < length)
170            {
171              throw new LDAPException(ResultCode.DECODING_ERROR,
172                                      ERR_OC_DECODE_CLOSE_NOT_AT_END.get(
173                                           objectClassString));
174            }
175            break;
176          }
177          else if (lowerToken.equals("name"))
178          {
179            if (nameList.isEmpty())
180            {
181              pos = skipSpaces(objectClassString, pos, length);
182              pos = readQDStrings(objectClassString, pos, length, nameList);
183            }
184            else
185            {
186              throw new LDAPException(ResultCode.DECODING_ERROR,
187                                      ERR_OC_DECODE_MULTIPLE_ELEMENTS.get(
188                                           objectClassString, "NAME"));
189            }
190          }
191          else if (lowerToken.equals("desc"))
192          {
193            if (descr == null)
194            {
195              pos = skipSpaces(objectClassString, pos, length);
196    
197              buffer = new StringBuilder();
198              pos = readQDString(objectClassString, pos, length, buffer);
199              descr = buffer.toString();
200            }
201            else
202            {
203              throw new LDAPException(ResultCode.DECODING_ERROR,
204                                      ERR_OC_DECODE_MULTIPLE_ELEMENTS.get(
205                                           objectClassString, "DESC"));
206            }
207          }
208          else if (lowerToken.equals("obsolete"))
209          {
210            if (obsolete == null)
211            {
212              obsolete = true;
213            }
214            else
215            {
216              throw new LDAPException(ResultCode.DECODING_ERROR,
217                                      ERR_OC_DECODE_MULTIPLE_ELEMENTS.get(
218                                           objectClassString, "OBSOLETE"));
219            }
220          }
221          else if (lowerToken.equals("sup"))
222          {
223            if (supList.isEmpty())
224            {
225              pos = skipSpaces(objectClassString, pos, length);
226              pos = readOIDs(objectClassString, pos, length, supList);
227            }
228            else
229            {
230              throw new LDAPException(ResultCode.DECODING_ERROR,
231                                      ERR_OC_DECODE_MULTIPLE_ELEMENTS.get(
232                                           objectClassString, "SUP"));
233            }
234          }
235          else if (lowerToken.equals("abstract"))
236          {
237            if (ocType == null)
238            {
239              ocType = ObjectClassType.ABSTRACT;
240            }
241            else
242            {
243              throw new LDAPException(ResultCode.DECODING_ERROR,
244                                      ERR_OC_DECODE_MULTIPLE_OC_TYPES.get(
245                                           objectClassString));
246            }
247          }
248          else if (lowerToken.equals("structural"))
249          {
250            if (ocType == null)
251            {
252              ocType = ObjectClassType.STRUCTURAL;
253            }
254            else
255            {
256              throw new LDAPException(ResultCode.DECODING_ERROR,
257                                      ERR_OC_DECODE_MULTIPLE_OC_TYPES.get(
258                                           objectClassString));
259            }
260          }
261          else if (lowerToken.equals("auxiliary"))
262          {
263            if (ocType == null)
264            {
265              ocType = ObjectClassType.AUXILIARY;
266            }
267            else
268            {
269              throw new LDAPException(ResultCode.DECODING_ERROR,
270                                      ERR_OC_DECODE_MULTIPLE_OC_TYPES.get(
271                                           objectClassString));
272            }
273          }
274          else if (lowerToken.equals("must"))
275          {
276            if (reqAttrs.isEmpty())
277            {
278              pos = skipSpaces(objectClassString, pos, length);
279              pos = readOIDs(objectClassString, pos, length, reqAttrs);
280            }
281            else
282            {
283              throw new LDAPException(ResultCode.DECODING_ERROR,
284                                      ERR_OC_DECODE_MULTIPLE_ELEMENTS.get(
285                                           objectClassString, "MUST"));
286            }
287          }
288          else if (lowerToken.equals("may"))
289          {
290            if (optAttrs.isEmpty())
291            {
292              pos = skipSpaces(objectClassString, pos, length);
293              pos = readOIDs(objectClassString, pos, length, optAttrs);
294            }
295            else
296            {
297              throw new LDAPException(ResultCode.DECODING_ERROR,
298                                      ERR_OC_DECODE_MULTIPLE_ELEMENTS.get(
299                                           objectClassString, "MAY"));
300            }
301          }
302          else if (lowerToken.startsWith("x-"))
303          {
304            pos = skipSpaces(objectClassString, pos, length);
305    
306            final ArrayList<String> valueList = new ArrayList<String>();
307            pos = readQDStrings(objectClassString, pos, length, valueList);
308    
309            final String[] values = new String[valueList.size()];
310            valueList.toArray(values);
311    
312            if (exts.containsKey(token))
313            {
314              throw new LDAPException(ResultCode.DECODING_ERROR,
315                                      ERR_OC_DECODE_DUP_EXT.get(objectClassString,
316                                                                token));
317            }
318    
319            exts.put(token, values);
320          }
321          else
322          {
323            throw new LDAPException(ResultCode.DECODING_ERROR,
324                                    ERR_OC_DECODE_UNEXPECTED_TOKEN.get(
325                                         objectClassString, token));
326          }
327        }
328    
329        description = descr;
330    
331        names = new String[nameList.size()];
332        nameList.toArray(names);
333    
334        superiorClasses = new String[supList.size()];
335        supList.toArray(superiorClasses);
336    
337        requiredAttributes = new String[reqAttrs.size()];
338        reqAttrs.toArray(requiredAttributes);
339    
340        optionalAttributes = new String[optAttrs.size()];
341        optAttrs.toArray(optionalAttributes);
342    
343        isObsolete = (obsolete != null);
344    
345        objectClassType = ocType;
346    
347        extensions = Collections.unmodifiableMap(exts);
348      }
349    
350    
351    
352      /**
353       * Creates a new object class with the provided information.
354       *
355       * @param  oid                 The OID for this object class.  It must not be
356       *                             {@code null}.
357       * @param  names               The set of names for this object class.  It may
358       *                             be {@code null} or empty if the object class
359       *                             should only be referenced by OID.
360       * @param  description         The description for this object class.  It may
361       *                             be {@code null} if there is no description.
362       * @param  isObsolete          Indicates whether this object class is declared
363       *                             obsolete.
364       * @param  superiorClasses     The names/OIDs of the superior classes for this
365       *                             object class.  It may be {@code null} or
366       *                             empty if there is no superior class.
367       * @param  objectClassType     The object class type for this object class.
368       * @param  requiredAttributes  The names/OIDs of the attributes which must be
369       *                             present in entries containing this object
370       *                             class.
371       * @param  optionalAttributes  The names/OIDs of the attributes which may be
372       *                             present in entries containing this object
373       *                             class.
374       * @param  extensions          The set of extensions for this object class.
375       *                             It may be {@code null} or empty if there should
376       *                             not be any extensions.
377       */
378      public ObjectClassDefinition(final String oid, final String[] names,
379                                   final String description,
380                                   final boolean isObsolete,
381                                   final String[] superiorClasses,
382                                   final ObjectClassType objectClassType,
383                                   final String[] requiredAttributes,
384                                   final String[] optionalAttributes,
385                                   final Map<String,String[]> extensions)
386      {
387        ensureNotNull(oid);
388    
389        this.oid             = oid;
390        this.isObsolete      = isObsolete;
391        this.description     = description;
392        this.objectClassType = objectClassType;
393    
394        if (names == null)
395        {
396          this.names = NO_STRINGS;
397        }
398        else
399        {
400          this.names = names;
401        }
402    
403        if (superiorClasses == null)
404        {
405          this.superiorClasses = NO_STRINGS;
406        }
407        else
408        {
409          this.superiorClasses = superiorClasses;
410        }
411    
412        if (requiredAttributes == null)
413        {
414          this.requiredAttributes = NO_STRINGS;
415        }
416        else
417        {
418          this.requiredAttributes = requiredAttributes;
419        }
420    
421        if (optionalAttributes == null)
422        {
423          this.optionalAttributes = NO_STRINGS;
424        }
425        else
426        {
427          this.optionalAttributes = optionalAttributes;
428        }
429    
430        if (extensions == null)
431        {
432          this.extensions = Collections.emptyMap();
433        }
434        else
435        {
436          this.extensions = Collections.unmodifiableMap(extensions);
437        }
438    
439        final StringBuilder buffer = new StringBuilder();
440        createDefinitionString(buffer);
441        objectClassString = buffer.toString();
442      }
443    
444    
445    
446      /**
447       * Constructs a string representation of this object class definition in the
448       * provided buffer.
449       *
450       * @param  buffer  The buffer in which to construct a string representation of
451       *                 this object class definition.
452       */
453      private void createDefinitionString(final StringBuilder buffer)
454      {
455        buffer.append("( ");
456        buffer.append(oid);
457    
458        if (names.length == 1)
459        {
460          buffer.append(" NAME '");
461          buffer.append(names[0]);
462          buffer.append('\'');
463        }
464        else if (names.length > 1)
465        {
466          buffer.append(" NAME (");
467          for (final String name : names)
468          {
469            buffer.append(" '");
470            buffer.append(name);
471            buffer.append('\'');
472          }
473          buffer.append(" )");
474        }
475    
476        if (description != null)
477        {
478          buffer.append(" DESC '");
479          encodeValue(description, buffer);
480          buffer.append('\'');
481        }
482    
483        if (isObsolete)
484        {
485          buffer.append(" OBSOLETE");
486        }
487    
488        if (superiorClasses.length == 1)
489        {
490          buffer.append(" SUP ");
491          buffer.append(superiorClasses[0]);
492        }
493        else if (superiorClasses.length > 1)
494        {
495          buffer.append(" SUP (");
496          for (int i=0; i < superiorClasses.length; i++)
497          {
498            if (i > 0)
499            {
500              buffer.append(" $ ");
501            }
502            else
503            {
504              buffer.append(' ');
505            }
506            buffer.append(superiorClasses[i]);
507          }
508          buffer.append(" )");
509        }
510    
511        if (objectClassType != null)
512        {
513          buffer.append(' ');
514          buffer.append(objectClassType.getName());
515        }
516    
517        if (requiredAttributes.length == 1)
518        {
519          buffer.append(" MUST ");
520          buffer.append(requiredAttributes[0]);
521        }
522        else if (requiredAttributes.length > 1)
523        {
524          buffer.append(" MUST (");
525          for (int i=0; i < requiredAttributes.length; i++)
526          {
527            if (i >0)
528            {
529              buffer.append(" $ ");
530            }
531            else
532            {
533              buffer.append(' ');
534            }
535            buffer.append(requiredAttributes[i]);
536          }
537          buffer.append(" )");
538        }
539    
540        if (optionalAttributes.length == 1)
541        {
542          buffer.append(" MAY ");
543          buffer.append(optionalAttributes[0]);
544        }
545        else if (optionalAttributes.length > 1)
546        {
547          buffer.append(" MAY (");
548          for (int i=0; i < optionalAttributes.length; i++)
549          {
550            if (i > 0)
551            {
552              buffer.append(" $ ");
553            }
554            else
555            {
556              buffer.append(' ');
557            }
558            buffer.append(optionalAttributes[i]);
559          }
560          buffer.append(" )");
561        }
562    
563        for (final Map.Entry<String,String[]> e : extensions.entrySet())
564        {
565          final String   name   = e.getKey();
566          final String[] values = e.getValue();
567          if (values.length == 1)
568          {
569            buffer.append(' ');
570            buffer.append(name);
571            buffer.append(" '");
572            encodeValue(values[0], buffer);
573            buffer.append('\'');
574          }
575          else
576          {
577            buffer.append(' ');
578            buffer.append(name);
579            buffer.append(" (");
580            for (final String value : values)
581            {
582              buffer.append(" '");
583              encodeValue(value, buffer);
584              buffer.append('\'');
585            }
586            buffer.append(" )");
587          }
588        }
589    
590        buffer.append(" )");
591      }
592    
593    
594    
595      /**
596       * Retrieves the OID for this object class.
597       *
598       * @return  The OID for this object class.
599       */
600      public String getOID()
601      {
602        return oid;
603      }
604    
605    
606    
607      /**
608       * Retrieves the set of names for this object class.
609       *
610       * @return  The set of names for this object class, or an empty array if it
611       *          does not have any names.
612       */
613      public String[] getNames()
614      {
615        return names;
616      }
617    
618    
619    
620      /**
621       * Retrieves the primary name that can be used to reference this object
622       * class.  If one or more names are defined, then the first name will be used.
623       * Otherwise, the OID will be returned.
624       *
625       * @return  The primary name that can be used to reference this object class.
626       */
627      public String getNameOrOID()
628      {
629        if (names.length == 0)
630        {
631          return oid;
632        }
633        else
634        {
635          return names[0];
636        }
637      }
638    
639    
640    
641      /**
642       * Indicates whether the provided string matches the OID or any of the names
643       * for this object class.
644       *
645       * @param  s  The string for which to make the determination.  It must not be
646       *            {@code null}.
647       *
648       * @return  {@code true} if the provided string matches the OID or any of the
649       *          names for this object class, or {@code false} if not.
650       */
651      public boolean hasNameOrOID(final String s)
652      {
653        for (final String name : names)
654        {
655          if (s.equalsIgnoreCase(name))
656          {
657            return true;
658          }
659        }
660    
661        return s.equalsIgnoreCase(oid);
662      }
663    
664    
665    
666      /**
667       * Retrieves the description for this object class, if available.
668       *
669       * @return  The description for this object class, or {@code null} if there is
670       *          no description defined.
671       */
672      public String getDescription()
673      {
674        return description;
675      }
676    
677    
678    
679      /**
680       * Indicates whether this object class is declared obsolete.
681       *
682       * @return  {@code true} if this object class is declared obsolete, or
683       *          {@code false} if it is not.
684       */
685      public boolean isObsolete()
686      {
687        return isObsolete;
688      }
689    
690    
691    
692      /**
693       * Retrieves the names or OIDs of the superior classes for this object class,
694       * if available.
695       *
696       * @return  The names or OIDs of the superior classes for this object class,
697       *          or an empty array if it does not have any superior classes.
698       */
699      public String[] getSuperiorClasses()
700      {
701        return superiorClasses;
702      }
703    
704    
705    
706      /**
707       * Retrieves the object class definitions for the superior object classes.
708       *
709       * @param  schema     The schema to use to retrieve the object class
710       *                    definitions.
711       * @param  recursive  Indicates whether to recursively include all of the
712       *                    superior object class definitions from superior classes.
713       *
714       * @return  The object class definitions for the superior object classes.
715       */
716      public Set<ObjectClassDefinition> getSuperiorClasses(final Schema schema,
717                                                           final boolean recursive)
718      {
719        final LinkedHashSet<ObjectClassDefinition> ocSet =
720             new LinkedHashSet<ObjectClassDefinition>();
721        for (final String s : superiorClasses)
722        {
723          final ObjectClassDefinition d = schema.getObjectClass(s);
724          if (d != null)
725          {
726            ocSet.add(d);
727            if (recursive)
728            {
729              getSuperiorClasses(schema, d, ocSet);
730            }
731          }
732        }
733    
734        return Collections.unmodifiableSet(ocSet);
735      }
736    
737    
738    
739      /**
740       * Recursively adds superior class definitions to the provided set.
741       *
742       * @param  schema  The schema to use to retrieve the object class definitions.
743       * @param  oc      The object class definition to be processed.
744       * @param  ocSet   The set to which the definitions should be added.
745       */
746      private static void getSuperiorClasses(final Schema schema,
747                                             final ObjectClassDefinition oc,
748                                             final Set<ObjectClassDefinition> ocSet)
749      {
750        for (final String s : oc.superiorClasses)
751        {
752          final ObjectClassDefinition d = schema.getObjectClass(s);
753          if (d != null)
754          {
755            ocSet.add(d);
756            getSuperiorClasses(schema, d, ocSet);
757          }
758        }
759      }
760    
761    
762    
763      /**
764       * Retrieves the object class type for this object class.
765       *
766       * @return  The object class type for this object class, or {@code null} if it
767       *          is not defined.
768       */
769      public ObjectClassType getObjectClassType()
770      {
771        return objectClassType;
772      }
773    
774    
775    
776      /**
777       * Retrieves the object class type for this object class, recursively
778       * examining superior classes if necessary to make the determination.
779       *
780       * @param  schema  The schema to use to retrieve the definitions for the
781       *                 superior object classes.
782       *
783       * @return  The object class type for this object class.
784       */
785      public ObjectClassType getObjectClassType(final Schema schema)
786      {
787        if (objectClassType != null)
788        {
789          return objectClassType;
790        }
791    
792        for (final String ocName : superiorClasses)
793        {
794          final ObjectClassDefinition d = schema.getObjectClass(ocName);
795          if (d != null)
796          {
797            return d.getObjectClassType(schema);
798          }
799        }
800    
801        return ObjectClassType.STRUCTURAL;
802      }
803    
804    
805    
806      /**
807       * Retrieves the names or OIDs of the attributes that are required to be
808       * present in entries containing this object class.  Note that this will not
809       * automatically include the set of required attributes from any superior
810       * classes.
811       *
812       * @return  The names or OIDs of the attributes that are required to be
813       *          present in entries containing this object class, or an empty array
814       *          if there are no required attributes.
815       */
816      public String[] getRequiredAttributes()
817      {
818        return requiredAttributes;
819      }
820    
821    
822    
823      /**
824       * Retrieves the attribute type definitions for the attributes that are
825       * required to be present in entries containing this object class, optionally
826       * including the set of required attribute types from superior classes.
827       *
828       * @param  schema                  The schema to use to retrieve the
829       *                                 attribute type definitions.
830       * @param  includeSuperiorClasses  Indicates whether to include definitions
831       *                                 for required attribute types in superior
832       *                                 object classes.
833       *
834       * @return  The attribute type definitions for the attributes that are
835       *          required to be present in entries containing this object class.
836       */
837      public Set<AttributeTypeDefinition> getRequiredAttributes(final Schema schema,
838                                               final boolean includeSuperiorClasses)
839      {
840        final HashSet<AttributeTypeDefinition> attrSet =
841             new HashSet<AttributeTypeDefinition>();
842        for (final String s : requiredAttributes)
843        {
844          final AttributeTypeDefinition d = schema.getAttributeType(s);
845          if (d != null)
846          {
847            attrSet.add(d);
848          }
849        }
850    
851        if (includeSuperiorClasses)
852        {
853          for (final String s : superiorClasses)
854          {
855            final ObjectClassDefinition d = schema.getObjectClass(s);
856            if (d != null)
857            {
858              getSuperiorRequiredAttributes(schema, d, attrSet);
859            }
860          }
861        }
862    
863        return Collections.unmodifiableSet(attrSet);
864      }
865    
866    
867    
868      /**
869       * Recursively adds the required attributes from the provided object class
870       * to the given set.
871       *
872       * @param  schema   The schema to use during processing.
873       * @param  oc       The object class to be processed.
874       * @param  attrSet  The set to which the attribute type definitions should be
875       *                  added.
876       */
877      private static void getSuperiorRequiredAttributes(final Schema schema,
878                               final ObjectClassDefinition oc,
879                               final Set<AttributeTypeDefinition> attrSet)
880      {
881        for (final String s : oc.requiredAttributes)
882        {
883          final AttributeTypeDefinition d = schema.getAttributeType(s);
884          if (d != null)
885          {
886            attrSet.add(d);
887          }
888        }
889    
890        for (final String s : oc.superiorClasses)
891        {
892          final ObjectClassDefinition d = schema.getObjectClass(s);
893          getSuperiorRequiredAttributes(schema, d, attrSet);
894        }
895      }
896    
897    
898    
899      /**
900       * Retrieves the names or OIDs of the attributes that may optionally be
901       * present in entries containing this object class.  Note that this will not
902       * automatically include the set of optional attributes from any superior
903       * classes.
904       *
905       * @return  The names or OIDs of the attributes that may optionally be present
906       *          in entries containing this object class, or an empty array if
907       *          there are no optional attributes.
908       */
909      public String[] getOptionalAttributes()
910      {
911        return optionalAttributes;
912      }
913    
914    
915    
916      /**
917       * Retrieves the attribute type definitions for the attributes that may
918       * optionally be present in entries containing this object class, optionally
919       * including the set of optional attribute types from superior classes.
920       *
921       * @param  schema                  The schema to use to retrieve the
922       *                                 attribute type definitions.
923       * @param  includeSuperiorClasses  Indicates whether to include definitions
924       *                                 for optional attribute types in superior
925       *                                 object classes.
926       *
927       * @return  The attribute type definitions for the attributes that may
928       *          optionally be present in entries containing this object class.
929       */
930      public Set<AttributeTypeDefinition> getOptionalAttributes(final Schema schema,
931                                               final boolean includeSuperiorClasses)
932      {
933        final HashSet<AttributeTypeDefinition> attrSet =
934             new HashSet<AttributeTypeDefinition>();
935        for (final String s : optionalAttributes)
936        {
937          final AttributeTypeDefinition d = schema.getAttributeType(s);
938          if (d != null)
939          {
940            attrSet.add(d);
941          }
942        }
943    
944        if (includeSuperiorClasses)
945        {
946          final Set<AttributeTypeDefinition> requiredAttrs =
947               getRequiredAttributes(schema, true);
948          for (final AttributeTypeDefinition d : requiredAttrs)
949          {
950            attrSet.remove(d);
951          }
952    
953          for (final String s : superiorClasses)
954          {
955            final ObjectClassDefinition d = schema.getObjectClass(s);
956            if (d != null)
957            {
958              getSuperiorOptionalAttributes(schema, d, attrSet, requiredAttrs);
959            }
960          }
961        }
962    
963        return Collections.unmodifiableSet(attrSet);
964      }
965    
966    
967    
968      /**
969       * Recursively adds the optional attributes from the provided object class
970       * to the given set.
971       *
972       * @param  schema       The schema to use during processing.
973       * @param  oc           The object class to be processed.
974       * @param  attrSet      The set to which the attribute type definitions should
975       *                      be added.
976       * @param  requiredSet  x
977       */
978      private static void getSuperiorOptionalAttributes(final Schema schema,
979                               final ObjectClassDefinition oc,
980                               final Set<AttributeTypeDefinition> attrSet,
981                               final Set<AttributeTypeDefinition> requiredSet)
982      {
983        for (final String s : oc.optionalAttributes)
984        {
985          final AttributeTypeDefinition d = schema.getAttributeType(s);
986          if ((d != null) && (! requiredSet.contains(d)))
987          {
988            attrSet.add(d);
989          }
990        }
991    
992        for (final String s : oc.superiorClasses)
993        {
994          final ObjectClassDefinition d = schema.getObjectClass(s);
995          getSuperiorOptionalAttributes(schema, d, attrSet, requiredSet);
996        }
997      }
998    
999    
1000    
1001      /**
1002       * Retrieves the set of extensions for this object class.  They will be mapped
1003       * from the extension name (which should start with "X-") to the set of values
1004       * for that extension.
1005       *
1006       * @return  The set of extensions for this object class.
1007       */
1008      public Map<String,String[]> getExtensions()
1009      {
1010        return extensions;
1011      }
1012    
1013    
1014    
1015      /**
1016       * {@inheritDoc}
1017       */
1018      @Override()
1019      public int hashCode()
1020      {
1021        return oid.hashCode();
1022      }
1023    
1024    
1025    
1026      /**
1027       * {@inheritDoc}
1028       */
1029      @Override()
1030      public boolean equals(final Object o)
1031      {
1032        if (o == null)
1033        {
1034          return false;
1035        }
1036    
1037        if (o == this)
1038        {
1039          return true;
1040        }
1041    
1042        if (! (o instanceof ObjectClassDefinition))
1043        {
1044          return false;
1045        }
1046    
1047        final ObjectClassDefinition d = (ObjectClassDefinition) o;
1048        return (oid.equals(d.oid) &&
1049             stringsEqualIgnoreCaseOrderIndependent(names, d.names) &&
1050             stringsEqualIgnoreCaseOrderIndependent(requiredAttributes,
1051                  d.requiredAttributes) &&
1052             stringsEqualIgnoreCaseOrderIndependent(optionalAttributes,
1053                  d.optionalAttributes) &&
1054             stringsEqualIgnoreCaseOrderIndependent(superiorClasses,
1055                  d.superiorClasses) &&
1056             bothNullOrEqual(objectClassType, d.objectClassType) &&
1057             bothNullOrEqualIgnoreCase(description, d.description) &&
1058             (isObsolete == d.isObsolete) &&
1059             extensionsEqual(extensions, d.extensions));
1060      }
1061    
1062    
1063    
1064      /**
1065       * Retrieves a string representation of this object class definition, in the
1066       * format described in RFC 4512 section 4.1.1.
1067       *
1068       * @return  A string representation of this object class definition.
1069       */
1070      @Override()
1071      public String toString()
1072      {
1073        return objectClassString;
1074      }
1075    }