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.BaseResource;
022import com.unboundid.scim.data.ResourceFactory;
023import com.unboundid.scim.sdk.InvalidResourceException;
024import com.unboundid.scim.sdk.SCIMConstants;
025import com.unboundid.scim.sdk.SCIMObject;
026
027import java.util.ArrayList;
028import java.util.Arrays;
029import java.util.Collection;
030import java.util.HashMap;
031import java.util.HashSet;
032import java.util.List;
033import java.util.Map;
034import java.util.Set;
035
036import static com.unboundid.scim.sdk.StaticUtils.toLowerCase;
037
038
039
040/**
041 * This class provides methods that describe the schema for a SCIM resource. It
042 * may be used to help read and write SCIM objects in their external XML and
043 * JSON representation, and to convert SCIM objects to and from LDAP entries.
044 */
045public class ResourceDescriptor extends BaseResource
046{
047  /**
048   * A <code>ResourceFactory</code> for creating <code>ResourceDescriptor</code>
049   * instances.
050   */
051  public static final ResourceFactory<ResourceDescriptor>
052      RESOURCE_DESCRIPTOR_FACTORY = new ResourceFactory<ResourceDescriptor>() {
053    /**
054     * {@inheritDoc}
055     */
056    public ResourceDescriptor createResource(
057        final ResourceDescriptor resourceDescriptor,
058        final SCIMObject scimObject) {
059      return new ResourceDescriptor(resourceDescriptor, scimObject);
060    }
061  };
062
063  /**
064   * A schema -> name -> AttributeDescriptor map to quickly look up
065   * attributes. The attribute descriptors are keyed by the lower case
066   * attribute name because attribute names are case-insensitive. Likewise,
067   * the schema key is lower case because schema URNs are case-insensitive.
068   */
069  private volatile Map<String,Map<String,AttributeDescriptor>> attributesCache;
070
071  /**
072   * Whether to use "strict mode" when looking up an attribute
073   * that doesn't exist in the attributesCache. Changed to false for better
074   * interoperability with SCIM implementations.
075   */
076  private boolean strictMode = false;
077
078  /**
079   * Constructs a new ResourceDescriptor from a existing SCIMObject.
080   *
081   * @param resourceDescriptor The Resource Schema descriptor.
082   * @param scimObject The SCIMObject containing the schema.
083   */
084  ResourceDescriptor(final ResourceDescriptor resourceDescriptor,
085                     final SCIMObject scimObject) {
086    super(resourceDescriptor, scimObject);
087  }
088
089  /**
090   * Constructs a new empty ResourceDescriptor.
091   *
092   * @param resourceDescriptor The Resource Schema descriptor.
093   */
094  private ResourceDescriptor(final ResourceDescriptor resourceDescriptor) {
095    super(resourceDescriptor);
096  }
097
098  /**
099   * Retrieves the attribute descriptor for a specified attribute.
100   *
101   * @param schema The attribute descriptor's associated schema URN.
102   * @param name The name of the attribute whose descriptor is to be retrieved.
103   *
104   * @return The attribute descriptor for the specified attribute.
105   * @throws InvalidResourceException if there is no such attribute.
106   */
107  public AttributeDescriptor getAttribute(final String schema,
108                                          final String name)
109      throws InvalidResourceException
110  {
111    final Map<String, Map<String, AttributeDescriptor>> attrCache =
112        getAttributesCache();
113    AttributeDescriptor attributeDescriptor = null;
114    Map<String, AttributeDescriptor> map = attrCache.get(toLowerCase(schema));
115    if(map != null)
116    {
117      attributeDescriptor = map.get(toLowerCase(name));
118    }
119    if(attributeDescriptor == null)
120    {
121      if (strictMode || SCIMConstants.SCHEMA_URI_CORE.equalsIgnoreCase(schema))
122      {
123        throw new InvalidResourceException("Attribute " + schema +
124            SCIMConstants.SEPARATOR_CHAR_QUALIFIED_ATTRIBUTE + name +
125            " is not defined for resource " + getName());
126      }
127      else
128      {
129        attributeDescriptor = AttributeDescriptor.createMultiValuedAttribute(
130            name, "value", null, schema, false, false, false,
131            CoreSchema.createMultiValuedValueDescriptor(
132                schema, AttributeDescriptor.DataType.STRING));
133      }
134    }
135    return attributeDescriptor;
136  }
137
138  /**
139   * Retrieves the attribute descriptor for a specified attribute, if the
140   * attribute name is defined.  Similar to <code>getAttribute</code> but
141   * returns null rather than throwing an exception if the attribute is
142   * not defined.
143   * @param schema The attribute descriptor's associated schema URI.
144   * @param name The name of the attribute whose descriptor is to be retrieved.
145   * @return The attribute descriptor for the specified attribute, or null
146   * if the attribute is not defined on the resource.
147   */
148  public AttributeDescriptor findAttribute(final String schema,
149                                           final String name)
150  {
151    final Map<String, Map<String, AttributeDescriptor>> attrCache =
152        getAttributesCache();
153    Map<String, AttributeDescriptor> map = attrCache.get(toLowerCase(schema));
154    return (map == null) ? null : map.get(toLowerCase(name));
155  }
156
157  /**
158   * Retrieves all the attribute descriptors of the provided schema defined
159   * in the resource.
160   *
161   * @param schema The name of the schema.
162   * @return All the attribute descriptors of the provided schema defined
163   * for this resource.
164   */
165  public Collection<AttributeDescriptor> getAttributes(final String schema)
166  {
167    final Map<String, Map<String, AttributeDescriptor>> attrCache =
168        getAttributesCache();
169    Map<String, AttributeDescriptor> map = attrCache.get(toLowerCase(schema));
170    if(map != null)
171    {
172      return map.values();
173    }
174    return null;
175  }
176
177  /**
178   * Retrieves the set of unique schemas for the attribute descriptors defined
179   * in the resource.
180   *
181   * @return The set of unique schemas for the attribute descriptors defined
182   * in the resource.
183   */
184  public Set<String> getAttributeSchemas()
185  {
186    return getAttributesCache().keySet();
187  }
188
189  /**
190   * Retrieves the schema for a specified attribute name.
191   *
192   * @param name The name of the attribute whose schema is to be found.
193   * @param preferredSchemas an ordered list of schemas to prefer when matching
194   *
195   * @return The schema for the specified attribute or {@code null} if there is
196   *         no such attribute name in any schema.
197   */
198  public String findAttributeSchema(final String name,
199                                    final String... preferredSchemas)
200  {
201    final Map<String, Map<String, AttributeDescriptor>> attrCache =
202        getAttributesCache();
203    Set<String> matchingSchemas = new HashSet<String>();
204    for (String schema : getAttributeSchemas())
205    {
206      Map<String, AttributeDescriptor> map = attrCache.get(toLowerCase(schema));
207      if (map!= null && map.keySet().contains(name))
208      {
209        matchingSchemas.add(schema);
210      }
211    }
212    if (matchingSchemas.isEmpty())
213    {
214      // Element does not belong to any known schema
215      return null;
216    }
217    List<String> preferredSchemaList;
218    if (preferredSchemas == null || preferredSchemas.length < 1)
219    {
220      preferredSchemaList = new ArrayList<String>(2);
221      preferredSchemaList.add(getSchema());
222      preferredSchemaList.add(SCIMConstants.SCHEMA_URI_CORE);
223    }
224    else
225    {
226      preferredSchemaList = Arrays.asList(preferredSchemas);
227    }
228    for (String preferredSchema : preferredSchemaList)
229    {
230      if (matchingSchemas.contains(preferredSchema))
231      {
232        return preferredSchema;
233      }
234    }
235    // If no preferred schema was found then just return the first from the list
236    return matchingSchemas.iterator().next();
237  }
238
239
240  /**
241   * Retrieve the name of the resource to be used in any external representation
242   * of the resource.
243   *
244   * @return Retrieve the name of the resource to be used in any external
245   *         representation of the resource. It is never {@code null}.
246   */
247  public String getName()
248  {
249    return getSingularAttributeValue(SCIMConstants.SCHEMA_URI_CORE, "name",
250        AttributeValueResolver.STRING_RESOLVER);
251  }
252
253  /**
254   * Sets the "strict mode" for this ResourceDescriptor. If strict mode is off,
255   * then a call to {@link #getAttribute(String, String)} where the requested
256   * attribute does not exist in the attributesCache will result in the method
257   * generating an AttributeDescriptor on the fly. If strict mode were on in
258   * this case, it would throw an exception because that attribute was not
259   * defined.
260   *
261   * @param strictMode a boolean indicating whether to use strict mode or not.
262   */
263  public void setStrictMode(final boolean strictMode)
264  {
265    this.strictMode = strictMode;
266  }
267
268  /**
269   * Gets the "strict mode" setting for this ResourceDescriptor. If strict mode
270   * is off, then a call to {@link #getAttribute(String, String)} where the
271   * requested attribute does not exist in the attributesCache will result in
272   * the method generating an AttributeDescriptor on the fly. If strict mode
273   * were on in this case, it would throw an exception because that attribute
274   * was not defined.
275   *
276   * @return boolean indicating whether strict mode is enabled.
277   */
278  public boolean isStrictMode()
279  {
280    return this.strictMode;
281  }
282
283  /**
284   * Sets the name of the resource to be used in any external representation
285   * of the resource.
286   *
287   * @param name The name of the resource to be used in any external
288   *             representation of the resource.
289   * @return this ResourceDescriptor.
290   */
291  private ResourceDescriptor setName(final String name)
292  {
293    try {
294      setSingularAttributeValue(SCIMConstants.SCHEMA_URI_CORE,
295          "name", AttributeValueResolver.STRING_RESOLVER, name);
296    } catch (InvalidResourceException e) {
297      // This should never happen as these are core attributes...
298      throw new RuntimeException(e);
299    }
300    return this;
301  }
302
303  /**
304   * Retrieves the list of all attribute descriptors defined in the resource.
305   *
306   * @return The list of attribute descriptors for the resource. It is never
307   *         {@code null}.
308   */
309  public Collection<AttributeDescriptor> getAttributes()
310  {
311    Collection<AttributeDescriptor> attributes =
312        getAttributeValues(SCIMConstants.SCHEMA_URI_CORE,
313            "attributes", AttributeDescriptor.ATTRIBUTE_DESCRIPTOR_RESOLVER);
314    return CoreSchema.addCommonAttributes(attributes);
315  }
316
317  /**
318   * Sets the list of attribute descriptors for the resource.
319   *
320   * @param attributes The list of attribute descriptors for the resource.
321   * @return this ResourceDescriptor.
322   */
323  private ResourceDescriptor setAttributes(
324      final Collection<AttributeDescriptor> attributes)
325  {
326    try {
327      setAttributeValues(SCIMConstants.SCHEMA_URI_CORE,
328          "attributes", AttributeDescriptor.ATTRIBUTE_DESCRIPTOR_RESOLVER,
329          attributes);
330      attributesCache = null;
331    } catch (InvalidResourceException e) {
332      // This should never happen as these are core attributes...
333      throw new RuntimeException(e);
334    }
335    return this;
336  }
337
338  /**
339   * Returns the resource's XML schema (namespace) name.
340   *
341   * @return The XML namespace name.
342   */
343  public String getSchema()
344  {
345    return toLowerCase(
346            getSingularAttributeValue(SCIMConstants.SCHEMA_URI_CORE, "schema",
347            AttributeValueResolver.STRING_RESOLVER));
348  }
349
350  /**
351   * Sets the resource's XML schema (namespace) name.
352   *
353   * @param schema The XML namespace name.
354   * @return this ResourceDescriptor.
355   */
356  private ResourceDescriptor setSchema(final String schema)
357  {
358    try {
359      setSingularAttributeValue(SCIMConstants.SCHEMA_URI_CORE,
360          "schema", AttributeValueResolver.STRING_RESOLVER, schema);
361    } catch (InvalidResourceException e) {
362      // This should never happen as these are core attributes...
363      throw new RuntimeException(e);
364    }
365    return this;
366  }
367
368  /**
369   * Retrieves the resource's human readable description.
370   *
371   * @return The resource's human readable description.
372   */
373  public String getDescription()
374  {
375    return getSingularAttributeValue(SCIMConstants.SCHEMA_URI_CORE,
376        "description", AttributeValueResolver.STRING_RESOLVER);
377  }
378
379  /**
380   * Sets the resource's human readable description.
381   *
382   * @param description The resource's human readable description.
383   * @return this ResourceDescriptor.
384   */
385  private ResourceDescriptor setDescription(final String description)
386  {
387    try {
388      setSingularAttributeValue(SCIMConstants.SCHEMA_URI_CORE,
389          "description", AttributeValueResolver.STRING_RESOLVER, description);
390    } catch (InvalidResourceException e) {
391      // This should never happen as these are core attributes...
392      throw new RuntimeException(e);
393    }
394    return this;
395  }
396
397  /**
398   * Retrieves the Resource's HTTP addressable endpoint relative to the
399   * Base URL.
400   *
401   * @return The Resource's HTTP addressable endpoint relative to the Base URL.
402   */
403  public String getEndpoint()
404  {
405    return getSingularAttributeValue(SCIMConstants.SCHEMA_URI_CORE,
406        "endpoint", AttributeValueResolver.STRING_RESOLVER);
407  }
408
409  /**
410   * Sets the Resource's HTTP addressable endpoint relative to the
411   * Base URL.
412   *
413   * @param endpoint The Resource's HTTP addressable endpoint relative to
414   *                 the Base URL.
415   * @return this ResourceDescriptor.
416   */
417  private ResourceDescriptor setEndpoint(final String endpoint)
418  {
419    try {
420      setSingularAttributeValue(SCIMConstants.SCHEMA_URI_CORE,
421          "endpoint", AttributeValueResolver.STRING_RESOLVER, endpoint);
422    } catch (InvalidResourceException e) {
423      // This should never happen as these are core attributes...
424      throw new RuntimeException(e);
425    }
426    return this;
427  }
428
429  /**
430   * Retrieves the attributesCache, using double-checked locking to avoid
431   * synchronization in the common case where the cache is already initialized.
432   *
433   * @return A snapshot of the attributesCache. The cache is regenerated when
434   *         {@link #setAttributes(java.util.Collection)} is called.
435   */
436  private Map<String, Map<String, AttributeDescriptor>> getAttributesCache()
437  {
438    Map<String, Map<String, AttributeDescriptor>> attrCache = attributesCache;
439    if (attrCache == null)
440    {
441      synchronized (this)
442      {
443        attrCache = attributesCache;
444        if (attrCache == null)
445        {
446          attrCache = new HashMap<String,
447              Map<String, AttributeDescriptor>>();
448          for(AttributeDescriptor attributeDescriptor : getAttributes())
449          {
450            final String lowerCaseSchema =
451                toLowerCase(attributeDescriptor.getSchema());
452            Map<String, AttributeDescriptor> map =
453                attrCache.get(lowerCaseSchema);
454            if(map == null)
455            {
456              map = new HashMap<String, AttributeDescriptor>();
457              attrCache.put(lowerCaseSchema, map);
458            }
459            map.put(toLowerCase(attributeDescriptor.getName()),
460                    attributeDescriptor);
461          }
462
463          attributesCache = attrCache;
464        }
465      }
466    }
467    return attrCache;
468  }
469
470  /**
471   * {@inheritDoc}
472   */
473  @Override
474  public int hashCode()
475  {
476    int hashCode = 31;
477    hashCode += hashCode * toLowerCase(getSchema()).hashCode();
478    hashCode += hashCode * toLowerCase(getName()).hashCode();
479    return hashCode;
480  }
481
482  /**
483   * {@inheritDoc}
484   */
485  @Override
486  public boolean equals(final Object obj)
487  {
488    if (this == obj)
489    {
490      return true;
491    }
492
493    if (!(obj instanceof ResourceDescriptor))
494    {
495      return false;
496    }
497
498    final ResourceDescriptor that = (ResourceDescriptor)obj;
499    final String thisSchema = getSchema();
500    final String thisName = getName();
501    final String thatSchema = that.getSchema();
502    final String thatName = that.getName();
503    if (thisSchema == null && thatSchema == null)
504    {
505      return thisName.equalsIgnoreCase(thatName);
506    }
507    else
508    {
509      return thisSchema != null && thatSchema != null &&
510          thisSchema.equalsIgnoreCase(thatSchema) &&
511          thisName.equalsIgnoreCase(thatName);
512    }
513  }
514
515  @Override
516  public String toString()
517  {
518    return "ResourceDescriptor{" +
519        "name='" + getName() + '\'' +
520        ", description='" + getDescription() +
521        ", schema='" + getSchema() + '\'' +
522        ", endpoint='" + getEndpoint() + '\'' +
523        ", attributes=" + getAttributes() +
524        '}';
525  }
526
527  /**
528   * Construct a new resource descriptor with the provided information.
529   * The resource attributes specified here should not include common core
530   * attributes (ie. id, externalId, meta) as these will be added automatically.
531   *
532   * @param name The addressable Resource endpoint name.
533   * @param description The Resource's human readable description.
534   * @param schema The Resource's associated schema URN
535   * @param endpoint The Resource's HTTP addressable endpoint relative
536   *                 to the Base URL.
537   * @param attributes Specifies the set of associated Resource attributes.
538   * @return The newly constructed resource descriptor.
539   */
540  public static ResourceDescriptor create(
541      final String name, final String description, final String schema,
542      final String endpoint, final AttributeDescriptor... attributes)
543  {
544    ResourceDescriptor resourceDescriptor =
545      new ResourceDescriptor(CoreSchema.RESOURCE_SCHEMA_DESCRIPTOR);
546    resourceDescriptor.setName(name);
547    resourceDescriptor.setDescription(description);
548    resourceDescriptor.setSchema(schema);
549    resourceDescriptor.setEndpoint(endpoint);
550    resourceDescriptor.setAttributes(Arrays.asList(attributes));
551
552    return resourceDescriptor;
553  }
554}