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.sdk;
019
020import com.unboundid.scim.schema.AttributeDescriptor;
021import com.unboundid.scim.schema.CoreSchema;
022import com.unboundid.scim.schema.ResourceDescriptor;
023
024import java.util.ArrayList;
025import java.util.Collection;
026import java.util.Collections;
027import java.util.HashMap;
028import java.util.LinkedHashMap;
029import java.util.List;
030import java.util.Map;
031import java.util.Set;
032
033import static com.unboundid.scim.sdk.StaticUtils.toLowerCase;
034
035
036
037/**
038 * This class represents a System for Cross-Domain Identity Management (SCIM)
039 * object. A SCIM object may be composed of common schema attributes and a
040 * collection of attributes from one or more additional schema definitions.
041 * This class is not designed to be thread-safe.
042 */
043public class SCIMObject
044{
045
046  /**
047   * The set of attributes in this object grouped by the URI of the schema to
048   * which they belong.
049   */
050  private final HashMap<String,LinkedHashMap<String,SCIMAttribute>> attributes;
051
052
053
054  /**
055   * Create an empty SCIM object that initially has no attributes. The type of
056   * resource is not specified.
057   */
058  public SCIMObject()
059  {
060    this.attributes =
061        new HashMap<String, LinkedHashMap<String, SCIMAttribute>>();
062  }
063
064
065  /**
066   * Create a new copy of the provided SCIM object.
067   *
068   * @param scimObject The SCIMObject to copy.
069   */
070  public SCIMObject(final SCIMObject scimObject)
071  {
072    // Since SCIMAttribute is immutable, just copy the maps.
073    this.attributes =
074        new HashMap<String, LinkedHashMap<String, SCIMAttribute>>();
075    for(Map.Entry<String, LinkedHashMap<String, SCIMAttribute>> entry :
076        scimObject.attributes.entrySet())
077    {
078      this.attributes.put(entry.getKey(),
079          new LinkedHashMap<String, SCIMAttribute>(entry.getValue()));
080    }
081  }
082
083
084
085  /**
086   * Retrieves the set of schemas currently contributing attributes to this
087   * object.
088   *
089   * @return  An immutable collection of the URIs of schemas currently
090   *          contributing attributes to this object.
091   */
092  public Set<String> getSchemas()
093  {
094    return Collections.unmodifiableSet(attributes.keySet());
095  }
096
097
098
099  /**
100   * Determines whether this object contains any attributes in the specified
101   * schema.
102   *
103   * @param schema  The URI of the schema for which to make the determination.
104   *                It must not be {@code null}.
105   *
106   * @return  {@code true} if this object contains any attributes in the
107   *          specified schema, or {@code false} if not.
108   */
109  public boolean hasSchema(final String schema)
110  {
111    return attributes.containsKey(toLowerCase(schema));
112  }
113
114
115
116  /**
117   * Retrieves the attribute with the specified name.
118   * <p>
119   * Note that this method does not support retrieving sub-attributes directly;
120   * you can retrieve a SCIMAttribute and then iterate over its values to get
121   * the sub-attributes, if the values are complex.
122   *
123   * @param schema  The URI of the schema containing the attribute to retrieve.
124   *
125   * @param name    The name of the attribute to retrieve. It must not be
126   *                {@code null}.
127   *
128   * @return  The requested attribute from this object, or {@code null} if the
129   *          specified attribute is not present in this object.
130   */
131  public SCIMAttribute getAttribute(final String schema, final String name)
132  {
133    final LinkedHashMap<String,SCIMAttribute> attrs =
134        attributes.get(toLowerCase(schema));
135
136    if (attrs == null)
137    {
138      return null;
139    }
140    else
141    {
142      return attrs.get(toLowerCase(name));
143    }
144  }
145
146
147
148  /**
149   * Retrieves the set of attributes in this object from the specified schema.
150   *
151   * @param schema  The URI of the schema whose attributes are to be retrieved.
152   *
153   * @return  An immutable collection of the attributes in this object from the
154   *          specified schema, or the empty collection if there are no such
155   *          attributes.
156   */
157  public Collection<SCIMAttribute> getAttributes(final String schema)
158  {
159    final LinkedHashMap<String, SCIMAttribute> attrs =
160        attributes.get(toLowerCase(schema));
161
162    if (attrs == null)
163    {
164      return Collections.emptyList();
165    }
166    else
167    {
168      return Collections.unmodifiableCollection(attrs.values());
169    }
170  }
171
172
173
174  /**
175   * Determines whether this object contains the specified attribute or
176   * sub-attribute.
177   *
178   * @param schema  The URI of the schema containing the attribute.
179   * @param name    The name of the attribute or sub-attribute.
180   *                Sub-attributes can be referenced with the standard
181   *                attribute notation (i.e. "attribute.subattribute".
182   *                This parameter must not be {@code null}.
183   *
184   * @return  {@code true} if this object contains the specified attribute, or
185   *          {@code false} if not.
186   */
187  public boolean hasAttribute(final String schema, final String name)
188  {
189    final LinkedHashMap<String, SCIMAttribute> attrs =
190        attributes.get(toLowerCase(schema));
191
192    AttributePath path = AttributePath.parse(name, schema);
193    String attrName = toLowerCase(path.getAttributeName());
194    String subAttrName = path.getSubAttributeName();
195
196    if (attrs != null && attrs.containsKey(attrName))
197    {
198      if (subAttrName != null)
199      {
200        SCIMAttribute attr = attrs.get(attrName);
201        for (SCIMAttributeValue value : attr.getValues())
202        {
203          if (value.isComplex() && value.hasAttribute(subAttrName))
204          {
205            return true;
206          }
207        }
208      }
209      else
210      {
211        return true;
212      }
213    }
214
215    return false;
216  }
217
218
219
220  /**
221   * Adds the provided attribute to this object. If this object already contains
222   * an attribute with the same name from the same schema, then the provided
223   * attribute will not be added.
224   *
225   * @param attribute  The attribute to be added. It must not be {@code null}.
226   *
227   * @return  {@code true} if the object was updated, or {@code false} if the
228   *          object already contained an attribute with the same name.
229   */
230  public boolean addAttribute(final SCIMAttribute attribute)
231  {
232    final String lowerCaseSchema = toLowerCase(attribute.getSchema());
233    final String lowerCaseName = toLowerCase(attribute.getName());
234
235    LinkedHashMap<String,SCIMAttribute> attrs = attributes.get(lowerCaseSchema);
236    if (attrs == null)
237    {
238      attrs = new LinkedHashMap<String, SCIMAttribute>();
239      attrs.put(lowerCaseName, attribute);
240      attributes.put(lowerCaseSchema, attrs);
241      return true;
242    }
243    else
244    {
245      if (attrs.containsKey(lowerCaseName))
246      {
247        return false;
248      }
249      else
250      {
251        attrs.put(lowerCaseName, attribute);
252        return true;
253      }
254    }
255  }
256
257
258
259  /**
260   * Adds the provided attribute to this object, replacing any existing
261   * attribute with the same name.
262   *
263   * @param attribute  The attribute to be added. It must not be {@code null}.
264   */
265  public void setAttribute(final SCIMAttribute attribute)
266  {
267    final String lowerCaseSchema = toLowerCase(attribute.getSchema());
268    final String lowerCaseName = toLowerCase(attribute.getName());
269
270    LinkedHashMap<String,SCIMAttribute> attrs = attributes.get(lowerCaseSchema);
271    if (attrs == null)
272    {
273      attrs = new LinkedHashMap<String, SCIMAttribute>();
274      attrs.put(lowerCaseName, attribute);
275      attributes.put(lowerCaseSchema, attrs);
276    }
277    else
278    {
279      attrs.put(lowerCaseName, attribute);
280    }
281  }
282
283
284
285  /**
286   * Removes the specified attribute or sub-attribute from this object.
287   *
288   * @param schema  The URI of the schema to which the attribute belongs.
289   * @param name    The name of the attribute or sub-attribute to remove.
290   *                Sub-attributes can be referenced with the standard
291   *                attribute notation (i.e. "attribute.subattribute".
292   *                This parameter must not be {@code null}.
293   *
294   * @return  {@code true} if the attribute was removed from the object, or
295   *          {@code false} if it was not present.
296   */
297  public boolean removeAttribute(final String schema, final String name)
298  {
299    final String lowerCaseSchema = toLowerCase(schema);
300    LinkedHashMap<String,SCIMAttribute> attrs = attributes.get(lowerCaseSchema);
301    if (attrs == null)
302    {
303      return false;
304    }
305    else
306    {
307      AttributePath path = AttributePath.parse(name, schema);
308      String attrName = toLowerCase(path.getAttributeName());
309      String subAttrName = path.getSubAttributeName();
310
311      boolean removed = false;
312
313      if (subAttrName != null)
314      {
315        //We are removing a sub-attribute
316        if (attrs.containsKey(attrName))
317        {
318          SCIMAttribute attr = attrs.get(attrName);
319          List<SCIMAttributeValue> finalComplexValues =
320                  new ArrayList<SCIMAttributeValue>(4);
321
322          for(SCIMAttributeValue value : attr.getValues())
323          {
324            if(value.isComplex())
325            {
326              Map<String, SCIMAttribute> subAttrMap = value.getAttributes();
327              List<SCIMAttribute> attrList = new ArrayList<SCIMAttribute>(10);
328
329              //We need to keep track if only normative sub-attributes are left
330              //after the sub-attribute removal; if that is the case, then the
331              //entire attribute value should be removed since it no longer has
332              //a value.
333              boolean nonNormativeSubAttributeExists = false;
334
335              for(String n : subAttrMap.keySet())
336              {
337                if(!n.equalsIgnoreCase(subAttrName))
338                {
339                  attrList.add(subAttrMap.get(n));
340
341                  if (!n.equals("type") && !n.equals("primary") &&
342                      !n.equals("operation") && !n.equals("display"))
343                  {
344                    nonNormativeSubAttributeExists = true;
345                  }
346                }
347                else
348                {
349                  removed = true;
350                }
351              }
352
353              if(!attrList.isEmpty() && nonNormativeSubAttributeExists)
354              {
355                SCIMAttributeValue newComplexValue =
356                        SCIMAttributeValue.createComplexValue(attrList);
357                finalComplexValues.add(newComplexValue);
358              }
359            }
360          }
361
362          if (removed)
363          {
364            if(!finalComplexValues.isEmpty())
365            {
366              SCIMAttribute finalAttr = SCIMAttribute.create(
367                      attr.getAttributeDescriptor(), finalComplexValues.toArray(
368                      new SCIMAttributeValue[finalComplexValues.size()]));
369              attrs.put(attrName, finalAttr);
370            }
371            else
372            {
373              //After removing the specified sub-attribute, there are no values
374              //left, so the entire attribute should be removed.
375              attrs.remove(attrName);
376            }
377          }
378        }
379      }
380      else
381      {
382        removed = (attrs.remove(attrName) != null);
383      }
384
385      if(removed && attrs.isEmpty())
386      {
387        attributes.remove(lowerCaseSchema);
388      }
389
390      return removed;
391    }
392  }
393
394
395
396  /**
397   * Determine whether this object matches the provided filter parameters.
398   *
399   * @param filter  The filter parameters to compare against the object.
400   *
401   * @return  {@code true} if this object matches the provided filter, and
402   *          {@code false} otherwise.
403   */
404  public boolean matchesFilter(final SCIMFilter filter)
405  {
406    final SCIMFilterType type = filter.getFilterType();
407    final List<SCIMFilter> components = filter.getFilterComponents();
408
409    switch(type)
410    {
411      case AND:
412        for(SCIMFilter component : components)
413        {
414          if(!matchesFilter(component))
415          {
416            return false;
417          }
418        }
419        return true;
420      case OR:
421        for(SCIMFilter component : components)
422        {
423          if(matchesFilter(component))
424          {
425            return true;
426          }
427        }
428        return false;
429    }
430
431    final String schema = filter.getFilterAttribute().getAttributeSchema();
432    final String attributeName = filter.getFilterAttribute().getAttributeName();
433
434    final SCIMAttribute attribute = getAttribute(schema, attributeName);
435    return attribute != null && attribute.matchesFilter(filter);
436
437  }
438
439
440
441  /**
442   * Check this object for potential schema violations based on the provided
443   * resource descriptor.
444   *
445   *
446   * @param resourceDescriptor The ResourceDescriptor to check against.
447   * @param includeCommonAttributes Whether to enforce the schema for common
448   *                                attributes like id and meta.
449   * @throws InvalidResourceException If a schema violation is found.
450   */
451  public void checkSchema(final ResourceDescriptor resourceDescriptor,
452                          final boolean includeCommonAttributes)
453      throws InvalidResourceException
454  {
455    // Make sure all required attributes are present
456    for(String schema : resourceDescriptor.getAttributeSchemas())
457    {
458      for(AttributeDescriptor attributeDescriptor :
459          resourceDescriptor.getAttributes(schema))
460      {
461        if(!includeCommonAttributes &&
462            (attributeDescriptor.equals(CoreSchema.ID_DESCRIPTOR) ||
463                attributeDescriptor.equals(CoreSchema.META_DESCRIPTOR) ||
464                attributeDescriptor.equals(CoreSchema.EXTERNAL_ID_DESCRIPTOR)))
465        {
466          continue;
467        }
468
469        if (attributeDescriptor.isReadOnly())
470        {
471          continue;
472        }
473
474        SCIMAttribute attribute =
475            getAttribute(schema, attributeDescriptor.getName());
476        if(attributeDescriptor.isRequired() && attribute == null)
477        {
478          throw new InvalidResourceException("Attribute '" +
479              schema + ":" + attributeDescriptor.getName() +
480              "' is required");
481        }
482
483        Collection<AttributeDescriptor> subAttributes =
484            attributeDescriptor.getSubAttributes();
485        if(subAttributes != null && attribute != null)
486        {
487          // Make sure all required sub-attributes are present as well
488          for(AttributeDescriptor subAttribute : subAttributes)
489          {
490            if(subAttribute.isRequired())
491            {
492              if(attributeDescriptor.isMultiValued())
493              {
494                for(SCIMAttributeValue value : attribute.getValues())
495                {
496                  if(!value.hasAttribute(subAttribute.getName()))
497                  {
498                    throw new InvalidResourceException("Sub-Attribute '" +
499                        schema + ":" + attributeDescriptor.getName() + "." +
500                        subAttribute.getName() + "' is required for all " +
501                        "values of the multi-valued attribute");
502                  }
503                }
504              }
505              else
506              {
507                if(!attribute.getValue().hasAttribute(subAttribute.getName()))
508                {
509                  throw new InvalidResourceException("Sub-Attribute '" +
510                      schema + ":" + attributeDescriptor.getName() + "." +
511                      subAttribute.getName() + "' is required");
512                }
513              }
514            }
515          }
516        }
517      }
518    }
519  }
520
521
522
523  /**
524   * {@inheritDoc}
525   */
526  @Override
527  public boolean equals(final Object o) {
528    if (this == o) {
529      return true;
530    }
531    if (o == null || getClass() != o.getClass()) {
532      return false;
533    }
534
535    SCIMObject that = (SCIMObject) o;
536
537    return attributes.equals(that.attributes);
538
539  }
540
541
542  /**
543   * {@inheritDoc}
544   */
545  @Override
546  public int hashCode() {
547    return attributes.hashCode();
548  }
549
550
551  /**
552   * {@inheritDoc}
553   */
554  @Override
555  public String toString() {
556    return "SCIMObject{" +
557      "attributes=" + attributes +
558      '}';
559  }
560}