001    /*
002     * Copyright 2011-2012 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    
018    package com.unboundid.scim.schema;
019    
020    import com.unboundid.scim.data.AttributeValueResolver;
021    import com.unboundid.scim.data.Entry;
022    import com.unboundid.scim.sdk.InvalidResourceException;
023    import com.unboundid.scim.sdk.SCIMAttribute;
024    import com.unboundid.scim.sdk.SCIMAttributeValue;
025    import com.unboundid.scim.sdk.SCIMConstants;
026    
027    import java.util.ArrayList;
028    import java.util.Arrays;
029    import java.util.Collection;
030    import java.util.LinkedHashMap;
031    import java.util.List;
032    import java.util.Map;
033    
034    import 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     */
044    public 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          final List<SCIMAttribute> attributes =
124              new ArrayList<SCIMAttribute>(6);
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          attributes.add(SCIMAttribute.create(
136              attributeDescriptor.getSubAttribute("description"),
137              SCIMAttributeValue.createStringValue(value.getDescription())));
138    
139          attributes.add(SCIMAttribute.create(
140              attributeDescriptor.getSubAttribute("readOnly"),
141              SCIMAttributeValue.createBooleanValue(value.isReadOnly())));
142    
143          attributes.add(SCIMAttribute.create(
144              attributeDescriptor.getSubAttribute("required"),
145              SCIMAttributeValue.createBooleanValue(value.isRequired())));
146    
147          attributes.add(SCIMAttribute.create(
148              attributeDescriptor.getSubAttribute("caseExact"),
149              SCIMAttributeValue.createBooleanValue(value.isCaseExact())));
150    
151          if(value.getCanonicalValues() != null)
152          {
153            final AttributeDescriptor canonicalValuesAttributeDescriptor =
154                attributeDescriptor.getSubAttribute("canonicalValues");
155            final SCIMAttributeValue[] canonicalValues =
156                new SCIMAttributeValue[value.getCanonicalValues().size()];
157            int i = 0;
158            for(Entry<String> canonicalValue : value.getCanonicalValues())
159            {
160              canonicalValues[i++] = Entry.STRINGS_RESOLVER.fromInstance(
161                  canonicalValuesAttributeDescriptor, canonicalValue);
162            }
163            attributes.add(SCIMAttribute.create(
164                canonicalValuesAttributeDescriptor, canonicalValues));
165          }
166    
167          return SCIMAttributeValue.createComplexValue(attributes);
168        }
169    
170        /**
171         * {@inheritDoc}
172         */
173        @Override
174        public AttributeDescriptor toInstance(final SCIMAttributeValue value) {
175          return new AttributeDescriptor(
176              value.getSubAttributeValue("name",
177                                         AttributeValueResolver.STRING_RESOLVER),
178              DataType.parse(value.getSubAttributeValue(
179                  "type",
180                  AttributeValueResolver.STRING_RESOLVER)),
181              false, null,
182              value.getSubAttributeValue("description",
183                                         AttributeValueResolver.STRING_RESOLVER),
184              schema,
185              value.getSubAttributeValue("readOnly",
186                                         AttributeValueResolver.BOOLEAN_RESOLVER),
187              value.getSubAttributeValue("required",
188                                         AttributeValueResolver.BOOLEAN_RESOLVER),
189              value.getSubAttributeValue("caseExact",
190                                         AttributeValueResolver.BOOLEAN_RESOLVER),
191              value.getSubAttributeValues("canonicalValues",
192                                          Entry.STRINGS_RESOLVER), null);
193        }
194      }
195    
196      /**
197       * The <code>AttributeValueResolver</code> that resolves SCIM attribute values
198       * to/from <code>AttributeDescriptor</code> instances.
199       */
200      public static final AttributeValueResolver<AttributeDescriptor>
201          ATTRIBUTE_DESCRIPTOR_RESOLVER = new AttributeDescriptorResolver(false);
202    
203      static final class AttributeDescriptorResolver extends
204          AttributeValueResolver<AttributeDescriptor>
205      {
206        private final boolean allowNesting;
207    
208        /**
209         * Create a new AttributeDescriptorResolver.
210         *
211         * @param allowNesting <code>true</code> to allow nesting of complex
212         *                     types or <code>false</code> otherwise.
213         */
214        AttributeDescriptorResolver(final boolean allowNesting) {
215          this.allowNesting = allowNesting;
216        }
217    
218        /**
219         * {@inheritDoc}
220         */
221        @Override
222        public AttributeDescriptor toInstance(final SCIMAttributeValue value) {
223          String schemaValue = value.getSubAttributeValue("schema",
224              AttributeValueResolver.STRING_RESOLVER);
225          boolean multiValued = value.getSubAttributeValue("multiValued",
226              AttributeValueResolver.BOOLEAN_RESOLVER);
227          String multiValuedChildName = null;
228          if(multiValued)
229          {
230            multiValuedChildName = value.getSubAttributeValue(
231                "multiValuedAttributeChildName",
232                AttributeValueResolver.STRING_RESOLVER);
233          }
234          return new AttributeDescriptor(
235              value.getSubAttributeValue("name",
236                                         AttributeValueResolver.STRING_RESOLVER),
237              DataType.parse(value.getSubAttributeValue(
238                  "type",
239                  AttributeValueResolver.STRING_RESOLVER)),
240              multiValued, multiValuedChildName,
241              value.getSubAttributeValue("description",
242                                         AttributeValueResolver.STRING_RESOLVER),
243              schemaValue,
244              value.getSubAttributeValue("readOnly",
245                                         AttributeValueResolver.BOOLEAN_RESOLVER),
246              value.getSubAttributeValue("required",
247                                         AttributeValueResolver.BOOLEAN_RESOLVER),
248              value.getSubAttributeValue("caseExact",
249                  AttributeValueResolver.BOOLEAN_RESOLVER), null,
250              value.getSubAttributeValues(
251                  allowNesting ? "attributes" : "subAttributes",
252                  allowNesting ? this :
253                      new SubAttributeDescriptorResolver(schemaValue)));
254        }
255    
256        /**
257         * {@inheritDoc}
258         */
259        @Override
260        public SCIMAttributeValue fromInstance(
261            final AttributeDescriptor attributeDescriptor,
262            final AttributeDescriptor value) throws InvalidResourceException {
263    
264          final List<SCIMAttribute> attributes =
265              new ArrayList<SCIMAttribute>(10);
266    
267          attributes.add(SCIMAttribute.create(
268              attributeDescriptor.getSubAttribute("name"),
269              SCIMAttributeValue.createStringValue(value.getName())));
270    
271          attributes.add(SCIMAttribute.create(
272              attributeDescriptor.getSubAttribute("type"),
273              SCIMAttributeValue.createStringValue(
274                  value.getDataType().toString())));
275    
276          attributes.add(SCIMAttribute.create(
277              attributeDescriptor.getSubAttribute("multiValued"),
278              SCIMAttributeValue.createBooleanValue(value.isMultiValued())));
279    
280          if(value.isMultiValued())
281          {
282            attributes.add(SCIMAttribute.create(
283              attributeDescriptor.getSubAttribute("multiValuedAttributeChildName"),
284              SCIMAttributeValue.createStringValue(
285                  value.getMultiValuedChildName())));
286          }
287    
288          attributes.add(SCIMAttribute.create(
289              attributeDescriptor.getSubAttribute("description"),
290              SCIMAttributeValue.createStringValue(value.getDescription())));
291    
292          attributes.add(SCIMAttribute.create(
293              attributeDescriptor.getSubAttribute("schema"),
294              SCIMAttributeValue.createStringValue(value.getSchema())));
295    
296          attributes.add(SCIMAttribute.create(
297              attributeDescriptor.getSubAttribute("readOnly"),
298              SCIMAttributeValue.createBooleanValue(value.isReadOnly())));
299    
300          attributes.add(SCIMAttribute.create(
301              attributeDescriptor.getSubAttribute("required"),
302              SCIMAttributeValue.createBooleanValue(value.isRequired())));
303    
304          attributes.add(SCIMAttribute.create(
305              attributeDescriptor.getSubAttribute("caseExact"),
306              SCIMAttributeValue.createBooleanValue(value.isCaseExact())));
307    
308          if(value.getSubAttributes() != null)
309          {
310            final AttributeDescriptor subAttributesDescriptor =
311                allowNesting ? attributeDescriptor :
312                attributeDescriptor.getSubAttribute("subAttributes");
313            final SCIMAttributeValue[] subAttributeValues =
314                new SCIMAttributeValue[value.getSubAttributes().size()];
315            int i = 0;
316            for(AttributeDescriptor subAttribute : value.getSubAttributes())
317            {
318              subAttributeValues[i++] = (allowNesting ? this :
319                  new SubAttributeDescriptorResolver(value.getSchema())).
320                  fromInstance(subAttributesDescriptor, subAttribute);
321            }
322            attributes.add(SCIMAttribute.create(
323                subAttributesDescriptor, subAttributeValues));
324          }
325    
326          return SCIMAttributeValue.createComplexValue(attributes);
327        }
328      }
329    
330      private final String schema;
331    
332      private final String name;
333    
334      private final String description;
335    
336      private final boolean readOnly;
337    
338      private final boolean required;
339    
340      private final boolean multiValued;
341    
342      private final String multiValuedChildName;
343    
344      private final boolean caseExact;
345    
346      private final DataType dataType;
347    
348      private final Map<String, AttributeDescriptor> subAttributes;
349    
350      private final Collection<Entry<String>> canonicalValues;
351    
352      /**
353       * Construct a new AttributeDescriptor instance with the provided info.
354       *
355       * @param name                 The attribute's name.
356       * @param dataType             The attribute's data type.
357       * @param multiValued          Whether the attribute is multiValued.
358       * @param multiValuedChildName String value specifying the child XML element
359       *                             name.
360       * @param description          The attribute's human readable description.
361       * @param schema               The attribute's associated schema.
362       * @param readOnly             Whether the attribute is mutable.
363       * @param required             Whether the attribute is required.
364       * @param caseExact            Whether the string attribute is case sensitive.
365       * @param canonicalValues      A list of canonical type values.
366       * @param subAttributes        A list specifying the contained attributes.
367       */
368      private AttributeDescriptor(final String name, final DataType dataType,
369                                  final boolean multiValued,
370                                  final String multiValuedChildName,
371                                  final String description, final String schema,
372                                  final boolean readOnly, final boolean required,
373                                  final boolean caseExact,
374                                  final Collection<Entry<String>> canonicalValues,
375                                final Collection<AttributeDescriptor> subAttributes)
376      {
377        this.name = name;
378        this.dataType = dataType;
379        this.multiValued = multiValued;
380        this.multiValuedChildName = multiValuedChildName;
381        this.description = description;
382        this.schema = schema;
383        this.readOnly = readOnly;
384        this.required = required;
385        this.caseExact = caseExact;
386    
387        if(canonicalValues != null && !canonicalValues.isEmpty())
388        {
389          this.canonicalValues = canonicalValues;
390        }
391        else
392        {
393          this.canonicalValues = null;
394        }
395    
396        if(subAttributes != null && !subAttributes.isEmpty())
397        {
398          this.subAttributes =
399              new LinkedHashMap<String, AttributeDescriptor>(subAttributes.size());
400          for(AttributeDescriptor attributeDescriptor : subAttributes)
401          {
402            this.subAttributes.put(toLowerCase(
403                attributeDescriptor.getName()),
404                attributeDescriptor);
405          }
406        }
407        else
408        {
409          this.subAttributes = null;
410        }
411      }
412    
413    
414      /**
415       * The URI for the schema that defines the SCIM attribute.
416       *
417       * @return The URI for the schema that defines the SCIM attribute.
418       *         It is never {@code null}.
419       */
420      public String getSchema() {
421        return schema;
422      }
423    
424    
425      /**
426       * The attribute name to be used in any external representation of the SCIM
427       * attribute.
428       *
429       * @return The attribute name to be used in any external representation of
430       *         the SCIM attribute. It is never {@code null}.
431       */
432      public String getName() {
433        return name;
434      }
435    
436      /**
437       * Indicates whether the attribute is a multi-valued attribute.
438       *
439       * @return {@code true} if the attribute is multi-valued.
440       */
441      public boolean isMultiValued() {
442        return multiValued;
443      }
444    
445    
446      /**
447       * The child XML element name for multi-valued attributes; e.g., the
448       * 'emails' attribute value is 'email', 'phoneNumbers', is 'phoneNumber'.
449       *
450       * @return  The child XML element name or {@code null} if this attribute
451       *          is not multi-valued.
452       */
453      public String getMultiValuedChildName() {
454        return multiValuedChildName;
455      }
456    
457      /**
458       * Retrieves the set of descriptors for subordinate attributes of a complex
459       * attribute.
460       *
461       * @return The set of descriptors for subordinate attributes of a complex
462       *         attribute, or {@code null} if the attribute is not a complex
463       *         attribute.
464       */
465      public Collection<AttributeDescriptor> getSubAttributes()
466      {
467        return subAttributes == null ? null : subAttributes.values();
468      }
469    
470    
471      /**
472       * Retrieves the attribute descriptor for a specified subordinate attribute
473       * of a complex attribute.
474       *
475       * @param externalName The external name of the subordinate attribute for
476       *                     which a descriptor is required.
477       * @return The attribute descriptor for the specified subordinate attribute.
478       * @throws InvalidResourceException if there is no such attribute.
479       */
480      public AttributeDescriptor getSubAttribute(final String externalName)
481          throws InvalidResourceException
482      {
483        // TODO: Should we have a strict and non strict mode?
484        AttributeDescriptor subAttribute =
485            subAttributes == null ? null :
486            subAttributes.get(toLowerCase(externalName));
487        if(subAttribute == null)
488        {
489          throw new InvalidResourceException("Sub-attribute " + externalName +
490              " is not defined for attribute " + schema +
491            SCIMConstants.SEPARATOR_CHAR_QUALIFIED_ATTRIBUTE + name);
492        }
493        return subAttribute;
494      }
495    
496    
497      /**
498       * Retrieves a list of canonical type values.
499       *
500       * @return A list of canonical type values or <code>null</code> if the
501       *         attribute is not a multi-valued attribute or if they are not
502       *         specified.
503       */
504      public Collection<Entry<String>> getCanonicalValues()
505      {
506        return canonicalValues;
507      }
508    
509    
510      /**
511       * Retrieve the data type for this attribute.
512       *
513       * @return  The data type for this attribute, or {@code null} if the attribute
514       *          is not a simple attribute.
515       */
516      public DataType getDataType()
517      {
518        return dataType;
519      }
520    
521    
522    
523      /**
524       * Retrieves this attribute's human readable description.
525       *
526       * @return This attribute's human redable description.
527       */
528      public String getDescription()
529      {
530        return description;
531      }
532    
533      /**
534       * Specifies if this attribute is mutable.
535       *
536       * @return <code>false</code> if this attribute is mutable or
537       *         <code>true</code> otherwise.
538       */
539      public boolean isReadOnly()
540      {
541        return readOnly;
542      }
543    
544      /**
545       * Specifies if this attribute is required.
546       *
547       * @return <code>true</code> if this attribute is required for
548       *         <code>false</code> otherwise.
549       */
550      public boolean isRequired()
551      {
552        return required;
553      }
554    
555      /**
556       * Specifies if the string attribute is case sensitive.
557       *
558       * @return <code>true</code> if this attribute is case sensitive or
559       *         <code>false</code> otherwise.
560       */
561      public boolean isCaseExact()
562      {
563        return caseExact;
564      }
565    
566      @Override
567      public String toString()
568      {
569        return "AttributeDescriptor{" +
570            "schema='" + getSchema() + '\'' +
571            ", name='" + getName() + '\'' +
572            ", description='" + getDescription() + '\'' +
573            ", multiValued=" + isMultiValued() +
574            ", dataType=" + getDataType() +
575            ", isRequired=" + isRequired() +
576            ", isReadOnly=" + isReadOnly() +
577            ", isCaseExact=" + isCaseExact() +
578            '}';
579      }
580    
581      /**
582       * {@inheritDoc}
583       */
584      @Override
585      public int hashCode()
586      {
587        int hashCode = 0;
588    
589        hashCode += toLowerCase(schema).hashCode();
590        hashCode += toLowerCase(name).hashCode();
591    
592        return hashCode;
593      }
594    
595      /**
596       * {@inheritDoc}
597       */
598      @Override
599      public boolean equals(final Object obj)
600      {
601        if (this == obj)
602        {
603          return true;
604        }
605    
606        if (!(obj instanceof AttributeDescriptor))
607        {
608          return false;
609        }
610    
611        final AttributeDescriptor that = (AttributeDescriptor)obj;
612        if (this.schema == null && that.schema == null)
613        {
614          return this.name.equalsIgnoreCase(that.name);
615        }
616        else
617        {
618          return this.schema != null && that.schema != null &&
619              this.schema.equalsIgnoreCase(that.schema) &&
620              this.name.equalsIgnoreCase(that.name);
621        }
622      }
623    
624      /**
625       * Create a new simple attribute descriptor with the provided
626       * information.
627       *
628       * @param name            The attribute's name.
629       * @param dataType        The attribute's data type.
630       * @param description     The attribute's human readable description.
631       * @param schema          The attribute's associated schema.
632       * @param readOnly        Whether the attribute is mutable.
633       * @param required        Whether the attribute is required.
634       * @param caseExact       Whether the string attribute is case sensitive.
635       * @param canonicalValues A collection of canonical values.
636       * @return                A new singular simple attribute descriptor with the
637       *                        provided information.
638       */
639      public static AttributeDescriptor createSubAttribute(
640          final String name, final DataType dataType, final String description,
641          final String schema, final boolean readOnly, final boolean required,
642          final boolean caseExact, final String... canonicalValues)
643      {
644        final Collection<Entry<String>> values;
645        if(canonicalValues != null && canonicalValues.length > 0)
646        {
647          values = new ArrayList<Entry<String>>(canonicalValues.length);
648          for(String canonicalValue : canonicalValues)
649          {
650            values.add(new Entry<String>(canonicalValue, null, false));
651          }
652        }
653        else
654        {
655          values = null;
656        }
657        return new AttributeDescriptor(name, dataType, false, null,
658            description, schema, readOnly, required, caseExact, values, null);
659      }
660    
661    
662    
663      /**
664       * Create a new complex attribute descriptor with the provided information.
665       *
666       *
667       * @param name                 The attribute's name.
668       * @param dataType             The attribute's data type.
669       *                             attribute is multi-valued or {@code null}.
670       * @param description          The attribute's human readable description.
671       * @param schema               The attribute's associated schema.
672       * @param readOnly             Whether the attribute is mutable.
673       * @param required             Whether the attribute is required.
674       * @param caseExact            Whether the string attribute is case sensitive.
675       * @param subAttributes        A list specifying the contained attributes.
676       * @return                     A new singular complex attribute descriptor
677       *                             with the provided information.
678       */
679      public static AttributeDescriptor createAttribute(
680          final String name, final DataType dataType, final String description,
681          final String schema, final boolean readOnly, final boolean required,
682          final boolean caseExact, final AttributeDescriptor... subAttributes)
683      {
684        return newAttribute(name, null, dataType, description, schema, readOnly,
685            required, caseExact, subAttributes);
686      }
687    
688    
689    
690      /**
691       * Create a new multi-valued attribute descriptor with the provided
692       * information. The normative sub-attributes for multi-valued attributes
693       * (ie. type, primary, display, operation, value) will be added.
694       *
695       *
696       * @param name                 The attribute's name.
697       * @param multiValuedChildName The attribute's child XML element name if this.
698       * @param dataType             The attribute's data type.
699       *                             attribute is multi-valued or {@code null}.
700       * @param description          The attribute's human readable description.
701       * @param schema               The attribute's associated schema.
702       * @param readOnly             Whether the attribute is mutable.
703       * @param required             Whether the attribute is required.
704       * @param caseExact            Whether the string attribute is case sensitive.
705       * @param canonicalValues The list of canonical values for the type attribute.
706       * @param subAttributes        A list specifying the contained attributes.
707       * @return                     A new singular complex attribute descriptor
708       *                             with the provided information.
709       */
710      public static AttributeDescriptor createMultiValuedAttribute(
711          final String name, final String multiValuedChildName,
712          final DataType dataType, final String description, final String schema,
713          final boolean readOnly, final boolean required, final boolean caseExact,
714          final String[] canonicalValues,
715          final AttributeDescriptor... subAttributes)
716      {
717        return newAttribute(name, multiValuedChildName, dataType,
718            description, schema, readOnly, required, caseExact,
719            CoreSchema.addCommonMultiValuedSubAttributes(
720                dataType, canonicalValues, subAttributes));
721      }
722    
723    
724    
725      /**
726       * Create a new attribute descriptor with the provided information.
727       *
728       *
729       * @param name                 The attribute's name.
730       * @param multiValuedChildName The attribute's child XML element name if this.
731       * @param dataType             The attribute's data type.
732       *                             attribute is multi-valued or {@code null}.
733       * @param description          The attribute's human readable description.
734       * @param schema               The attribute's associated schema.
735       * @param readOnly             Whether the attribute is mutable.
736       * @param required             Whether the attribute is required.
737       * @param caseExact            Whether the string attribute is case sensitive.
738       * @param subAttributes        A list specifying the contained attributes.
739       * @return                     A new singular complex attribute descriptor
740       *                             with the provided information.
741       */
742      static AttributeDescriptor newAttribute(
743          final String name, final String multiValuedChildName,
744          final DataType dataType, final String description, final String schema,
745          final boolean readOnly, final boolean required, final boolean caseExact,
746          final AttributeDescriptor... subAttributes)
747      {
748        return new AttributeDescriptor(name, dataType, multiValuedChildName != null,
749            multiValuedChildName, description, schema, readOnly, required,
750            caseExact, null, Arrays.asList(subAttributes));
751      }
752    }