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