001/*
002 * Copyright 2011-2016 UnboundID Corp.
003 *
004 * This program is free software; you can redistribute it and/or modify
005 * it under the terms of the GNU General Public License (GPLv2 only)
006 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
007 * as published by the Free Software Foundation.
008 *
009 * This program is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
012 * GNU General Public License for more details.
013 *
014 * You should have received a copy of the GNU General Public License
015 * along with this program; if not, see <http://www.gnu.org/licenses>.
016 */
017
018package com.unboundid.scim.schema;
019
020import com.unboundid.scim.data.AttributeValueResolver;
021import com.unboundid.scim.data.Entry;
022import com.unboundid.scim.sdk.InvalidResourceException;
023import com.unboundid.scim.sdk.SCIMAttribute;
024import com.unboundid.scim.sdk.SCIMAttributeValue;
025import com.unboundid.scim.sdk.SCIMConstants;
026
027import java.util.ArrayList;
028import java.util.Arrays;
029import java.util.Collection;
030import java.util.LinkedHashMap;
031import java.util.List;
032import java.util.Map;
033
034import static com.unboundid.scim.sdk.StaticUtils.toLowerCase;
035
036
037
038/**
039 * This class provides methods that describe the schema for a SCIM attribute.
040 * It may be used to help read and write SCIM attributes in their external XML
041 * and JSON representation, and to convert SCIM attributes to and from LDAP
042 * attributes.
043 */
044public final class AttributeDescriptor {
045
046  /**
047   * Defines the set of well known SCIM supported datatypes.
048   */
049  public static enum DataType {
050    /**
051     * String data type.
052     */
053    STRING,
054    /**
055     * Boolean data type.
056     */
057    BOOLEAN,
058    /**
059     * Date Time data type.
060     */
061    DATETIME,
062    /**
063     * Decimal data type.
064     */
065    DECIMAL,
066    /**
067     * Integer data type.
068     */
069    INTEGER,
070    /**
071     * Binary data type.
072     */
073    BINARY,
074    /**
075     * Complex data type.
076     */
077    COMPLEX;
078
079    /**
080     * Parses a supplied data type into a SCIM defined data type.
081     * @param type The type to convert
082     *
083     * @return The DataType or null if not supported
084     */
085    public static DataType parse(final String type) {
086      try {
087        return DataType.valueOf(type.toUpperCase());
088      } catch (Exception e) {
089        return null;
090      }
091    }
092
093    /**
094     * {@inheritDoc}
095     */
096    @Override
097    public String toString() {
098      return super.toString().toLowerCase();
099    }
100  }
101
102  static final class SubAttributeDescriptorResolver
103      extends AttributeValueResolver<AttributeDescriptor>
104  {
105    private final String schema;
106
107    /**
108     * Construct a new sub attribute resolver.
109     *
110     * @param schema The schema of the parent attribute.
111     */
112    SubAttributeDescriptorResolver(final String schema) {
113      this.schema = schema;
114    }
115
116    /**
117     * {@inheritDoc}
118     */
119    @Override
120    public SCIMAttributeValue fromInstance(
121        final AttributeDescriptor attributeDescriptor,
122        final AttributeDescriptor value) throws InvalidResourceException {
123
124      final List<SCIMAttribute> attributes = new ArrayList<SCIMAttribute>(9);
125
126      attributes.add(SCIMAttribute.create(
127          attributeDescriptor.getSubAttribute("name"),
128          SCIMAttributeValue.createStringValue(value.getName())));
129
130      attributes.add(SCIMAttribute.create(
131          attributeDescriptor.getSubAttribute("type"),
132          SCIMAttributeValue.createStringValue(
133              value.getDataType().toString())));
134
135
136      attributes.add(SCIMAttribute.create(
137          attributeDescriptor.getSubAttribute("multiValued"),
138          SCIMAttributeValue.createBooleanValue(value.isMultiValued())));
139
140      if (value.isMultiValued())
141      {
142        attributes.add(SCIMAttribute.create(
143          attributeDescriptor.getSubAttribute("multiValuedAttributeChildName"),
144          SCIMAttributeValue.createStringValue(
145                  value.getMultiValuedChildName())));
146      }
147
148      attributes.add(SCIMAttribute.create(
149          attributeDescriptor.getSubAttribute("description"),
150          SCIMAttributeValue.createStringValue(value.getDescription())));
151
152      attributes.add(SCIMAttribute.create(
153          attributeDescriptor.getSubAttribute("readOnly"),
154          SCIMAttributeValue.createBooleanValue(value.isReadOnly())));
155
156      attributes.add(SCIMAttribute.create(
157          attributeDescriptor.getSubAttribute("required"),
158          SCIMAttributeValue.createBooleanValue(value.isRequired())));
159
160      attributes.add(SCIMAttribute.create(
161          attributeDescriptor.getSubAttribute("caseExact"),
162          SCIMAttributeValue.createBooleanValue(value.isCaseExact())));
163
164      if(value.getCanonicalValues() != null)
165      {
166        final AttributeDescriptor canonicalValuesAttributeDescriptor =
167            attributeDescriptor.getSubAttribute("canonicalValues");
168        final SCIMAttributeValue[] canonicalValues =
169            new SCIMAttributeValue[value.getCanonicalValues().size()];
170        int i = 0;
171        for(Entry<String> canonicalValue : value.getCanonicalValues())
172        {
173          canonicalValues[i++] = Entry.STRINGS_RESOLVER.fromInstance(
174              canonicalValuesAttributeDescriptor, canonicalValue);
175        }
176        attributes.add(SCIMAttribute.create(
177            canonicalValuesAttributeDescriptor, canonicalValues));
178      }
179
180      return SCIMAttributeValue.createComplexValue(attributes);
181    }
182
183    /**
184     * {@inheritDoc}
185     */
186    @Override
187    public AttributeDescriptor toInstance(final SCIMAttributeValue value) {
188
189      DataType dataType = DataType.parse(value.getSubAttributeValue(
190              "type", AttributeValueResolver.STRING_RESOLVER));
191
192      return new AttributeDescriptor(
193
194          value.getSubAttributeValue(
195             "name", AttributeValueResolver.STRING_RESOLVER),
196
197          dataType,
198
199          value.getSubAttributeValue(
200              "multiValued", AttributeValueResolver.BOOLEAN_RESOLVER),
201
202          value.getSubAttributeValue(
203              "multiValuedAttributeChildName",
204                  AttributeValueResolver.STRING_RESOLVER),
205
206          value.getSubAttributeValue(
207              "description", AttributeValueResolver.STRING_RESOLVER),
208
209          schema,
210
211          value.getSubAttributeValue(
212              "readOnly", AttributeValueResolver.BOOLEAN_RESOLVER),
213
214          value.getSubAttributeValue(
215              "required", AttributeValueResolver.BOOLEAN_RESOLVER),
216
217          value.getSubAttributeValue(
218              "caseExact", AttributeValueResolver.BOOLEAN_RESOLVER),
219
220          value.getSubAttributeValues(
221              "canonicalValues", Entry.STRINGS_RESOLVER),
222
223          Arrays.asList(AttributeDescriptor.createSubAttribute(
224               "value", dataType, "The attribute's significant value",
225                SCIMConstants.SCHEMA_URI_CORE, false, true, false)));
226    }
227  }
228
229  /**
230   * The <code>AttributeValueResolver</code> that resolves SCIM attribute values
231   * to/from <code>AttributeDescriptor</code> instances.
232   */
233  public static final AttributeValueResolver<AttributeDescriptor>
234      ATTRIBUTE_DESCRIPTOR_RESOLVER = new AttributeDescriptorResolver(false);
235
236  static final class AttributeDescriptorResolver extends
237      AttributeValueResolver<AttributeDescriptor>
238  {
239    private final boolean allowNesting;
240
241    /**
242     * Create a new AttributeDescriptorResolver.
243     *
244     * @param allowNesting <code>true</code> to allow nesting of complex
245     *                     types or <code>false</code> otherwise.
246     */
247    AttributeDescriptorResolver(final boolean allowNesting) {
248      this.allowNesting = allowNesting;
249    }
250
251    /**
252     * {@inheritDoc}
253     */
254    @Override
255    public AttributeDescriptor toInstance(final SCIMAttributeValue value) {
256      String schemaValue = value.getSubAttributeValue(
257          "schema", AttributeValueResolver.STRING_RESOLVER);
258
259      boolean multiValued = value.getSubAttributeValue(
260          "multiValued", AttributeValueResolver.BOOLEAN_RESOLVER);
261
262      String multiValuedChildName = null;
263      if (multiValued)
264      {
265        multiValuedChildName = value.getSubAttributeValue(
266            "multiValuedAttributeChildName",
267            AttributeValueResolver.STRING_RESOLVER);
268      }
269
270      return new AttributeDescriptor(
271
272          value.getSubAttributeValue(
273              "name", AttributeValueResolver.STRING_RESOLVER),
274
275          DataType.parse(value.getSubAttributeValue(
276              "type", AttributeValueResolver.STRING_RESOLVER)),
277
278          multiValued, multiValuedChildName,
279
280          value.getSubAttributeValue(
281              "description", AttributeValueResolver.STRING_RESOLVER),
282
283          schemaValue,
284
285          value.getSubAttributeValue(
286              "readOnly", AttributeValueResolver.BOOLEAN_RESOLVER),
287
288          value.getSubAttributeValue(
289              "required", AttributeValueResolver.BOOLEAN_RESOLVER),
290
291          value.getSubAttributeValue(
292              "caseExact", AttributeValueResolver.BOOLEAN_RESOLVER),
293
294          null,
295
296          value.getSubAttributeValues(
297              allowNesting ? "attributes" : "subAttributes",
298              allowNesting ? this :
299                  new SubAttributeDescriptorResolver(schemaValue)));
300    }
301
302    /**
303     * {@inheritDoc}
304     */
305    @Override
306    public SCIMAttributeValue fromInstance(
307        final AttributeDescriptor attributeDescriptor,
308        final AttributeDescriptor value) throws InvalidResourceException {
309
310      final List<SCIMAttribute> attributes =
311          new ArrayList<SCIMAttribute>(10);
312
313      attributes.add(SCIMAttribute.create(
314          attributeDescriptor.getSubAttribute("name"),
315          SCIMAttributeValue.createStringValue(value.getName())));
316
317      attributes.add(SCIMAttribute.create(
318          attributeDescriptor.getSubAttribute("type"),
319          SCIMAttributeValue.createStringValue(
320              value.getDataType().toString())));
321
322      attributes.add(SCIMAttribute.create(
323          attributeDescriptor.getSubAttribute("multiValued"),
324          SCIMAttributeValue.createBooleanValue(value.isMultiValued())));
325
326      if(value.isMultiValued())
327      {
328        attributes.add(SCIMAttribute.create(
329          attributeDescriptor.getSubAttribute("multiValuedAttributeChildName"),
330          SCIMAttributeValue.createStringValue(
331              value.getMultiValuedChildName())));
332      }
333
334      attributes.add(SCIMAttribute.create(
335          attributeDescriptor.getSubAttribute("description"),
336          SCIMAttributeValue.createStringValue(value.getDescription())));
337
338      attributes.add(SCIMAttribute.create(
339          attributeDescriptor.getSubAttribute("schema"),
340          SCIMAttributeValue.createStringValue(value.getSchema())));
341
342      attributes.add(SCIMAttribute.create(
343          attributeDescriptor.getSubAttribute("readOnly"),
344          SCIMAttributeValue.createBooleanValue(value.isReadOnly())));
345
346      attributes.add(SCIMAttribute.create(
347          attributeDescriptor.getSubAttribute("required"),
348          SCIMAttributeValue.createBooleanValue(value.isRequired())));
349
350      attributes.add(SCIMAttribute.create(
351          attributeDescriptor.getSubAttribute("caseExact"),
352          SCIMAttributeValue.createBooleanValue(value.isCaseExact())));
353
354      if(value.subAttributes != null)
355      {
356        final AttributeDescriptor subAttributesDescriptor =
357            allowNesting ? attributeDescriptor :
358            attributeDescriptor.getSubAttribute("subAttributes");
359        final SCIMAttributeValue[] subAttributeValues =
360            new SCIMAttributeValue[value.subAttributes.size()];
361        int i = 0;
362        for(AttributeDescriptor subAttribute : value.subAttributes.values())
363        {
364          subAttributeValues[i++] = (allowNesting ? this :
365              new SubAttributeDescriptorResolver(value.getSchema())).
366              fromInstance(subAttributesDescriptor, subAttribute);
367        }
368        attributes.add(SCIMAttribute.create(
369            subAttributesDescriptor, subAttributeValues));
370      }
371
372      return SCIMAttributeValue.createComplexValue(attributes);
373    }
374  }
375
376  private final String schema;
377
378  private final String name;
379
380  private final String description;
381
382  private final boolean readOnly;
383
384  private final boolean required;
385
386  private final boolean multiValued;
387
388  private final String multiValuedChildName;
389
390  private final boolean caseExact;
391
392  private final DataType dataType;
393
394  private final Map<String, AttributeDescriptor> subAttributes;
395
396  private final Collection<Entry<String>> canonicalValues;
397
398  /**
399   * Construct a new AttributeDescriptor instance with the provided info.
400   *
401   * @param name                 The attribute's name.
402   * @param dataType             The attribute's data type.
403   * @param multiValued          Whether the attribute is multiValued.
404   * @param multiValuedChildName The child XML element name for multi-valued
405   *                             attributes.
406   * @param description          The attribute's human readable description.
407   * @param schema               The attribute's associated schema.
408   * @param readOnly             Whether the attribute is mutable.
409   * @param required             Whether the attribute is required.
410   * @param caseExact            Whether the string attribute is case sensitive.
411   * @param canonicalValues      A list of canonical type values.
412   * @param subAttributes        A list specifying the contained attributes.
413   */
414  private AttributeDescriptor(final String name, final DataType dataType,
415                              final boolean multiValued,
416                              final String multiValuedChildName,
417                              final String description, final String schema,
418                              final boolean readOnly, final boolean required,
419                              final boolean caseExact,
420                              final Collection<Entry<String>> canonicalValues,
421                            final Collection<AttributeDescriptor> subAttributes)
422  {
423    this.name = name;
424    this.dataType = dataType;
425    this.multiValued = multiValued;
426    this.multiValuedChildName = multiValuedChildName;
427    this.description = description;
428    this.schema = schema;
429    this.readOnly = readOnly;
430    this.required = required;
431    this.caseExact = caseExact;
432
433    if(canonicalValues != null && !canonicalValues.isEmpty())
434    {
435      this.canonicalValues = canonicalValues;
436    }
437    else
438    {
439      this.canonicalValues = null;
440    }
441
442    if(subAttributes != null && !subAttributes.isEmpty())
443    {
444      this.subAttributes =
445          new LinkedHashMap<String, AttributeDescriptor>(subAttributes.size());
446      for(AttributeDescriptor attributeDescriptor : subAttributes)
447      {
448        this.subAttributes.put(toLowerCase(
449            attributeDescriptor.getName()),
450            attributeDescriptor);
451      }
452    }
453    else
454    {
455      this.subAttributes = null;
456    }
457  }
458
459  /**
460   * The URI for the schema that defines the SCIM attribute.
461   *
462   * @return The URI for the schema that defines the SCIM attribute.
463   *         It is never {@code null}.
464   */
465  public String getSchema() {
466    return schema;
467  }
468
469  /**
470   * The attribute name to be used in any external representation of the SCIM
471   * attribute.
472   *
473   * @return The attribute name to be used in any external representation of
474   *         the SCIM attribute. It is never {@code null}.
475   */
476  public String getName() {
477    return name;
478  }
479
480  /**
481   * Indicates whether the attribute is a multi-valued attribute.
482   *
483   * @return {@code true} if the attribute is multi-valued.
484   */
485  public boolean isMultiValued() {
486    return multiValued;
487  }
488
489  /**
490   * The child XML element name for multi-valued attributes; e.g., the
491   * 'emails' attribute value is 'email', 'phoneNumbers', is 'phoneNumber'.
492   *
493   * @return  The child XML element name or {@code null} if this attribute
494   *          is not multi-valued.
495   */
496  public String getMultiValuedChildName() {
497    return multiValuedChildName;
498  }
499
500  /**
501   * Retrieves the set of descriptors for subordinate attributes of a complex
502   * attribute.
503   *
504   * @return The set of descriptors for subordinate attributes of a complex
505   *         attribute, or {@code null} if the attribute is not a complex
506   *         attribute.
507   */
508  public Collection<AttributeDescriptor> getSubAttributes()
509  {
510    Map<String, AttributeDescriptor> allSubAttributes =
511        CoreSchema.addNormativeSubAttributes(this, subAttributes);
512    return allSubAttributes == null ? null : allSubAttributes.values();
513  }
514
515  /**
516   * Retrieves the set of descriptors for subordinate attributes of a
517   * complex attribute.  This method does  not return normative
518   * sub-attributes that were not declared in the schema.
519   *
520   * @return The set of descriptors for subordinate attributes of a complex
521   *         attribute, or {@code null} if the attribute is not a complex
522   *         attribute.
523   */
524  public Collection<AttributeDescriptor> getDeclaredSubAttributes()
525  {
526    return subAttributes == null ? null : subAttributes.values();
527  }
528
529  /**
530   * Retrieves the attribute descriptor for a specified subordinate attribute
531   * of a complex attribute.
532   *
533   * @param externalName The external name of the subordinate attribute for
534   *                     which a descriptor is required.
535   * @return The attribute descriptor for the specified subordinate attribute.
536   * @throws InvalidResourceException if there is no such attribute.
537   */
538  public AttributeDescriptor getSubAttribute(final String externalName)
539      throws InvalidResourceException
540  {
541    // TODO: Should we have a strict and non strict mode?
542    Map<String, AttributeDescriptor> allSubAttributes =
543        CoreSchema.addNormativeSubAttributes(this, subAttributes);
544    AttributeDescriptor subAttribute =
545        allSubAttributes == null ? null :
546            allSubAttributes.get(toLowerCase(externalName));
547    if(subAttribute == null)
548    {
549      throw new InvalidResourceException("Sub-attribute " + externalName +
550          " is not defined for attribute " + schema +
551        SCIMConstants.SEPARATOR_CHAR_QUALIFIED_ATTRIBUTE + name);
552    }
553    return subAttribute;
554  }
555
556  /**
557   * Retrieves a list of canonical type values.
558   *
559   * @return A list of canonical type values or <code>null</code> if the
560   *         attribute is not a multi-valued attribute or if they are not
561   *         specified.
562   */
563  public Collection<Entry<String>> getCanonicalValues()
564  {
565    return canonicalValues;
566  }
567
568  /**
569   * Retrieve the data type for this attribute.
570   *
571   * @return  The data type for this attribute, or {@code null} if the attribute
572   *          is not a simple attribute.
573   */
574  public DataType getDataType()
575  {
576    return dataType;
577  }
578
579  /**
580   * Retrieves this attribute's human readable description.
581   *
582   * @return This attribute's human redable description.
583   */
584  public String getDescription()
585  {
586    return description;
587  }
588
589  /**
590   * Specifies if this attribute is mutable.
591   *
592   * @return <code>false</code> if this attribute is mutable or
593   *         <code>true</code> otherwise.
594   */
595  public boolean isReadOnly()
596  {
597    return readOnly;
598  }
599
600  /**
601   * Specifies if this attribute is required.
602   *
603   * @return <code>true</code> if this attribute is required for
604   *         <code>false</code> otherwise.
605   */
606  public boolean isRequired()
607  {
608    return required;
609  }
610
611  /**
612   * Specifies if the string attribute is case sensitive.
613   *
614   * @return <code>true</code> if this attribute is case sensitive or
615   *         <code>false</code> otherwise.
616   */
617  public boolean isCaseExact()
618  {
619    return caseExact;
620  }
621
622  @Override
623  public String toString()
624  {
625    return "AttributeDescriptor{" +
626        "schema='" + getSchema() + '\'' +
627        ", name='" + getName() + '\'' +
628        ", description='" + getDescription() + '\'' +
629        ", multiValued=" + isMultiValued() +
630        ", dataType=" + getDataType() +
631        ", isRequired=" + isRequired() +
632        ", isReadOnly=" + isReadOnly() +
633        ", isCaseExact=" + isCaseExact() +
634        '}';
635  }
636
637  /**
638   * {@inheritDoc}
639   */
640  @Override
641  public int hashCode()
642  {
643    int hashCode = 0;
644
645    hashCode += toLowerCase(schema).hashCode();
646    hashCode += toLowerCase(name).hashCode();
647
648    return hashCode;
649  }
650
651  /**
652   * {@inheritDoc}
653   */
654  @Override
655  public boolean equals(final Object obj)
656  {
657    if (this == obj)
658    {
659      return true;
660    }
661
662    if (!(obj instanceof AttributeDescriptor))
663    {
664      return false;
665    }
666
667    final AttributeDescriptor that = (AttributeDescriptor)obj;
668    if (this.schema == null && that.schema == null)
669    {
670      return this.name.equalsIgnoreCase(that.name);
671    }
672    else
673    {
674      return this.schema != null && that.schema != null &&
675          this.schema.equalsIgnoreCase(that.schema) &&
676          this.name.equalsIgnoreCase(that.name);
677    }
678  }
679
680  /**
681   * Create a new sub-attribute descriptor with the provided information.
682   *
683   * @param name            The attribute's name.
684   * @param dataType        The attribute's data type.
685   * @param description     The attribute's human readable description.
686   * @param schema          The attribute's associated schema.
687   * @param readOnly        Whether the attribute is mutable.
688   * @param required        Whether the attribute is required.
689   * @param caseExact       Whether the string attribute is case sensitive.
690   * @param canonicalValues A collection of canonical values.
691   * @return                A new singular sub-attribute descriptor with the
692   *                        provided information.
693   */
694  public static AttributeDescriptor createSubAttribute(
695      final String name, final DataType dataType, final String description,
696      final String schema, final boolean readOnly, final boolean required,
697      final boolean caseExact, final String... canonicalValues)
698  {
699    final Collection<Entry<String>> values;
700    if(canonicalValues != null && canonicalValues.length > 0)
701    {
702      values = new ArrayList<Entry<String>>(canonicalValues.length);
703      for(String canonicalValue : canonicalValues)
704      {
705        values.add(new Entry<String>(canonicalValue, null, false));
706      }
707    }
708    else
709    {
710      values = null;
711    }
712    return new AttributeDescriptor(name, dataType, false, null,
713        description, schema, readOnly, required, caseExact, values, null);
714  }
715
716  /**
717   * Create a new singular attribute descriptor with the provided information.
718   *
719   * @param name                 The attribute's name.
720   * @param dataType             The attribute's data type.
721   * @param description          The attribute's human readable description.
722   * @param schema               The attribute's associated schema.
723   * @param readOnly             Whether the attribute is mutable.
724   * @param required             Whether the attribute is required.
725   * @param caseExact            Whether the string attribute is case sensitive.
726   * @param subAttributes        A list specifying the contained attributes.
727   * @return                     A new singular attribute descriptor
728   *                             with the provided information.
729   */
730  public static AttributeDescriptor createAttribute(
731      final String name, final DataType dataType, final String description,
732      final String schema, final boolean readOnly, final boolean required,
733      final boolean caseExact, final AttributeDescriptor... subAttributes)
734  {
735    if(subAttributes != null)
736    {
737      for(AttributeDescriptor subAttribute : subAttributes)
738      {
739        if(subAttribute.getDataType() == DataType.COMPLEX)
740        {
741          throw new IllegalArgumentException("Complex sub-attributes are not " +
742              "allowed");
743        }
744      }
745    }
746    return newAttribute(name, null, dataType, description, schema, false,
747        readOnly, required, caseExact, subAttributes);
748  }
749
750  /**
751   * Create a new multi-valued attribute descriptor with the provided
752   * information. The normative sub-attributes for multi-valued attributes
753   * (ie. type, primary, display, operation, value) will be added.
754   *
755   * @param name                 The attribute's name.
756   * @param multiValuedChildName The child XML element name for multi-valued
757   *                             attributes.
758   * @param description          The attribute's human readable description.
759   * @param schema               The attribute's associated schema.
760   * @param readOnly             Whether the attribute is mutable.
761   * @param required             Whether the attribute is required.
762   * @param caseExact            Whether the string attribute is case sensitive.
763   * @param subAttributes        A list specifying the contained attributes.
764   * @return                     A new multi-valued attribute descriptor
765   *                             with the provided information.
766   */
767  public static AttributeDescriptor createMultiValuedAttribute(
768      final String name, final String multiValuedChildName,
769      final String description, final String schema,
770      final boolean readOnly, final boolean required, final boolean caseExact,
771      final AttributeDescriptor... subAttributes)
772  {
773    if(subAttributes != null)
774    {
775      for(AttributeDescriptor subAttribute : subAttributes)
776      {
777        if(subAttribute.getDataType() == DataType.COMPLEX)
778        {
779          throw new IllegalArgumentException("Complex sub-attributes are not " +
780              "allowed");
781        }
782      }
783    }
784    return newAttribute(name, multiValuedChildName, DataType.COMPLEX,
785        description, schema, true, readOnly, required, caseExact,
786        subAttributes);
787  }
788
789  /**
790   * Create a new attribute descriptor with the provided information.
791   *
792   * @param name                 The attribute's name.
793   * @param multiValuedChildName The child XML element name for multi-valued
794   *                             attributes.
795   * @param dataType             The attribute's data type.
796   * @param description          The attribute's human readable description.
797   * @param schema               The attribute's associated schema.
798   * @param multiValued          Whether the attribute is multiValued.
799   * @param readOnly             Whether the attribute is mutable.
800   * @param required             Whether the attribute is required.
801   * @param caseExact            Whether the string attribute is case sensitive.
802   * @param subAttributes        A list specifying the contained attributes.
803   * @return                     A new attribute descriptor with the provided
804   *                             information.
805   */
806  static AttributeDescriptor newAttribute(
807      final String name, final String multiValuedChildName,
808      final DataType dataType, final String description, final String schema,
809      final boolean multiValued, final boolean readOnly, final boolean required,
810      final boolean caseExact, final AttributeDescriptor... subAttributes)
811  {
812    Collection<AttributeDescriptor> subAttrs = null;
813    if (subAttributes != null)
814    {
815      subAttrs = Arrays.asList(subAttributes);
816    }
817    return new AttributeDescriptor(name, dataType, multiValued,
818        multiValuedChildName, description, schema, readOnly, required,
819        caseExact, null, subAttrs);
820  }
821}