001/*
002 * Copyright 2013 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.data.AttributeValueResolver;
021import com.unboundid.scim.data.BaseResource;
022import com.unboundid.scim.data.ResourceFactory;
023import com.unboundid.scim.schema.AttributeDescriptor;
024import com.unboundid.scim.schema.CoreSchema;
025import com.unboundid.scim.schema.ResourceDescriptor;
026
027import java.util.ArrayList;
028import java.util.Collections;
029import java.util.HashMap;
030import java.util.HashSet;
031import java.util.Iterator;
032import java.util.LinkedHashMap;
033import java.util.LinkedHashSet;
034import java.util.LinkedList;
035import java.util.List;
036import java.util.Map;
037import java.util.Set;
038
039import static com.unboundid.scim.sdk.StaticUtils.toLowerCase;
040
041/**
042 * This utility class may be used to generate a set of attribute
043 * modifications between two SCIM resources of the same type. This is
044 * especially useful for performing a PATCH request to modify a resource so it
045 * matches a target resource. For example:
046 *
047 * <pre>
048 * UserResource target = ...
049 * UserResource source = userEndpoint.getUser("someUser");
050 *
051 * Diff diff = Diff.generate(source, target);
052 *
053 * userEndpoint.update(source.getId(),
054 *                     diff.getAttributesToUpdate(),
055 *                     diff.getAttributesToDelete());
056 * </pre>
057 *
058 * You can also create a Diff instance from a SCIM partial resource which
059 * contains PATCH modifications. This can then be applied to a source resource
060 * to produce the target resource. For example:
061 *
062 * <pre>
063 * Diff diff = Diff.fromPartialResource(partialResource);
064 * BaseResource targetResource = diff.apply(sourceResource);
065 * </pre>
066 *
067 * @param <R> The type of resource instances the diff was generated from.
068 */
069public final class Diff<R extends BaseResource>
070
071{
072  private final List<SCIMAttribute> attributesToUpdate;
073  private final List<String> attributesToDelete;
074  private final ResourceDescriptor resourceDescriptor;
075
076  /**
077   * Construct a new Diff instance.
078   *
079   * @param resourceDescriptor The resource descriptor of resource the diff
080   *                           was generated from.
081   * @param attributesToDelete The list of attributes deleted from source
082   *                           resource.
083   * @param attributesToUpdate The list of attributes (and their new values) to
084   *                           update on the source resource.
085   */
086  Diff(final ResourceDescriptor resourceDescriptor,
087       final List<String> attributesToDelete,
088       final List<SCIMAttribute> attributesToUpdate)
089  {
090    this.resourceDescriptor = resourceDescriptor;
091    this.attributesToDelete = attributesToDelete;
092    this.attributesToUpdate = attributesToUpdate;
093  }
094
095  /**
096   * Retrieves the list of attributes deleted from the source resource. The
097   * values here are SCIM attribute names which may or may not contain the
098   * schema URN. These can be easily parsed using the
099   * {@link AttributePath#parse} method.
100   *
101   * @return The list of attributes deleted from source resource.
102   */
103  public List<String> getAttributesToDelete()
104  {
105    return attributesToDelete;
106  }
107
108  /**
109   * Retrieves the list of updated attributes (and their new values) to
110   * update on the source resource. These attributes will conform to
111   * Section 3.2.2 of the SCIM 1.1 specification (<i>draft-scim-api-01</i>),
112   * "Modifying Resources with PATCH".
113   *
114   * @return The list of attributes (and their new values) to update on the
115   *         source resource. Note that the attributes are in PATCH form (i.e.
116   *         they contain the values to merge into the resource).
117   */
118  public List<SCIMAttribute> getAttributesToUpdate()
119  {
120    return attributesToUpdate;
121  }
122
123  /**
124   * Applies the modifications from this {@link Diff} to the specified source
125   * resource, and returns the resulting SCIM resource.
126   *
127   * @param sourceResource the source resource to which the modifications should
128   *                       be applied.
129   * @param resourceFactory The ResourceFactory that should be used to create
130   *                        the new resource instance.
131   * @return the target resource with the modifications applied
132   */
133  public R apply(final R sourceResource,
134                 final ResourceFactory<R> resourceFactory)
135  {
136    final SCIMObject scimObject =
137            new SCIMObject(sourceResource.getScimObject());
138
139    if(attributesToDelete != null)
140    {
141      for(String attrPath : attributesToDelete)
142      {
143        AttributePath path = AttributePath.parse(attrPath);
144        String schema = path.getAttributeSchema();
145        String attrName = path.getAttributeName();
146        String subAttrName = path.getSubAttributeName();
147
148        if (subAttrName != null)
149        {
150          attrName = attrName + "." + subAttrName;
151        }
152
153        scimObject.removeAttribute(schema, attrName);
154      }
155    }
156
157    if (attributesToUpdate != null)
158    {
159      for(SCIMAttribute attr : attributesToUpdate)
160      {
161        if(attr.getAttributeDescriptor().isMultiValued())
162        {
163          //Go through and process all deleted values first
164          for(SCIMAttributeValue value : attr.getValues())
165          {
166            SCIMAttribute currentAttribute =
167                    scimObject.getAttribute(attr.getSchema(), attr.getName());
168
169            if(value.isComplex())
170            {
171              String operation = value.getSubAttributeValue("operation",
172                      AttributeValueResolver.STRING_RESOLVER);
173
174              if("delete".equalsIgnoreCase(operation))
175              {
176                //We are deleting a specific value from this
177                //multi-valued attribute
178                List<SCIMAttribute> subAttrs = new ArrayList<SCIMAttribute>();
179                Map<String, SCIMAttribute> subAttrMap = value.getAttributes();
180
181                for(String subAttrName : subAttrMap.keySet())
182                {
183                  if(!"operation".equalsIgnoreCase(subAttrName))
184                  {
185                    subAttrs.add(subAttrMap.get(subAttrName));
186                  }
187                }
188
189                SCIMAttributeValue valueToDelete =
190                        SCIMAttributeValue.createComplexValue(subAttrs);
191
192                if(currentAttribute != null)
193                {
194                  Set<SCIMAttributeValue> newValues =
195                          new HashSet<SCIMAttributeValue>();
196
197                  for(SCIMAttributeValue currentValue :
198                          currentAttribute.getValues())
199                  {
200                    if(!currentValue.equals(valueToDelete))
201                    {
202                      newValues.add(currentValue);
203                    }
204                  }
205
206                  if (!newValues.isEmpty())
207                  {
208                    SCIMAttribute finalAttribute = SCIMAttribute.create(
209                          attr.getAttributeDescriptor(), newValues.toArray(
210                                  new SCIMAttributeValue[newValues.size()]));
211
212                    scimObject.setAttribute(finalAttribute);
213                  }
214                  else
215                  {
216                    scimObject.removeAttribute(
217                            attr.getSchema(), attr.getName());
218                  }
219                }
220              }
221            }
222          }
223
224          //Now go through and merge in any new values
225          for (SCIMAttributeValue value : attr.getValues())
226          {
227            SCIMAttribute currentAttribute =
228                    scimObject.getAttribute(attr.getSchema(), attr.getName());
229
230            Set<SCIMAttributeValue> newValues =
231                    new HashSet<SCIMAttributeValue>();
232
233            if(value.isComplex())
234            {
235              String operation = value.getSubAttributeValue("operation",
236                      AttributeValueResolver.STRING_RESOLVER);
237
238              if("delete".equalsIgnoreCase(operation))
239              {
240                continue; //handled earlier
241              }
242
243              String type = value.getSubAttributeValue("type",
244                      AttributeValueResolver.STRING_RESOLVER);
245
246              //It's a complex multi-valued attribute. If a value with the same
247              //canonical type already exists, merge in the sub-attributes to
248              //that existing value. Otherwise, add a new complex value to the
249              //set of values.
250              if(currentAttribute != null)
251              {
252                SCIMAttributeValue valueToUpdate = null;
253                List<SCIMAttributeValue> finalValues =
254                        new LinkedList<SCIMAttributeValue>();
255
256                for(SCIMAttributeValue currentValue :
257                               currentAttribute.getValues())
258                {
259                  String currentType = currentValue.getSubAttributeValue(
260                          "type", AttributeValueResolver.STRING_RESOLVER);
261
262                  if (type != null && type.equalsIgnoreCase(currentType))
263                  {
264                    valueToUpdate = currentValue;
265                  }
266                  else if (!currentValue.equals(value))
267                  {
268                    finalValues.add(currentValue);
269                  }
270                }
271
272                if (valueToUpdate != null)
273                {
274                  Map<String, SCIMAttribute> subAttrMap = value.getAttributes();
275                  Map<String, SCIMAttribute> existingSubAttrMap =
276                          valueToUpdate.getAttributes();
277                  Map<String, SCIMAttribute> finalSubAttrs =
278                          new HashMap<String, SCIMAttribute>();
279
280                  for(String subAttrName : existingSubAttrMap.keySet())
281                  {
282                    if(subAttrMap.containsKey(subAttrName))
283                    {
284                      //Replace the subAttr with the incoming value
285                      finalSubAttrs.put(subAttrName,
286                              subAttrMap.get(subAttrName));
287                    }
288                    else
289                    {
290                      //Leave this subAttr as-is (it's not being modified)
291                      finalSubAttrs.put(subAttrName,
292                              existingSubAttrMap.get(subAttrName));
293                    }
294                  }
295
296                  //Add in any new sub-attributes that weren't in the
297                  //existing set
298                  for(String subAttrName : subAttrMap.keySet())
299                  {
300                    if(!finalSubAttrs.containsKey(subAttrName))
301                    {
302                      finalSubAttrs.put(subAttrName,
303                              subAttrMap.get(subAttrName));
304                    }
305                  }
306
307                  SCIMAttributeValue updatedValue = SCIMAttributeValue
308                          .createComplexValue(finalSubAttrs.values());
309                  finalValues.add(updatedValue);
310                }
311                else
312                {
313                  SCIMAttributeValue updatedValue = SCIMAttributeValue
314                          .createComplexValue(value.getAttributes().values());
315                  finalValues.add(updatedValue);
316                }
317
318                attr = SCIMAttribute.create(attr.getAttributeDescriptor(),
319                        finalValues.toArray(new SCIMAttributeValue[
320                                finalValues.size()]));
321              }
322
323              scimObject.setAttribute(attr);
324            }
325            else
326            {
327              //It's a simple multi-valued attribute. Merge this value into the
328              //existing values (if any) for the attribute
329              if(currentAttribute != null)
330              {
331                for(SCIMAttributeValue currentValue :
332                         currentAttribute.getValues())
333                {
334                  newValues.add(currentValue);
335                }
336              }
337              newValues.add(value);
338
339              SCIMAttribute finalAttribute = SCIMAttribute.create(
340                      attr.getAttributeDescriptor(), newValues.toArray(
341                      new SCIMAttributeValue[newValues.size()]));
342
343              scimObject.setAttribute(finalAttribute);
344            }
345          }
346        }
347        else //It's a single-valued attribute
348        {
349          if (scimObject.hasAttribute(attr.getSchema(), attr.getName()))
350          {
351            SCIMAttributeValue value = attr.getValue();
352            if (value.isComplex())
353            {
354              SCIMAttribute existingAttr =
355                      scimObject.getAttribute(attr.getSchema(), attr.getName());
356              SCIMAttributeValue existingValue = existingAttr.getValue();
357
358              Map<String,SCIMAttribute> subAttrMap = value.getAttributes();
359              Map<String,SCIMAttribute> existingSubAttrMap =
360                      existingValue.getAttributes();
361              Map<String,SCIMAttribute> finalSubAttrs =
362                      new HashMap<String,SCIMAttribute>();
363
364              for (String subAttrName : existingSubAttrMap.keySet())
365              {
366                if (subAttrMap.containsKey(subAttrName))
367                {
368                  finalSubAttrs.put(subAttrName,
369                          subAttrMap.get(subAttrName));
370                }
371                else
372                {
373                  finalSubAttrs.put(subAttrName,
374                          existingSubAttrMap.get(subAttrName));
375                }
376              }
377
378              //Add in any new sub-attributes that weren't in the existing set
379              for (String subAttrName : subAttrMap.keySet())
380              {
381                if (!finalSubAttrs.containsKey(subAttrName))
382                {
383                  finalSubAttrs.put(subAttrName, subAttrMap.get(subAttrName));
384                }
385              }
386
387              SCIMAttributeValue finalValue = SCIMAttributeValue
388                      .createComplexValue(finalSubAttrs.values());
389              attr = SCIMAttribute.create(
390                      attr.getAttributeDescriptor(), finalValue);
391            }
392          }
393          scimObject.setAttribute(attr);
394        }
395      }
396    }
397
398    return resourceFactory.createResource(resourceDescriptor, scimObject);
399  }
400
401  /**
402   * Retrieves the partial resource with the modifications that maybe sent in
403   * a PATCH request.
404   *
405   * @param resourceFactory The ResourceFactory that should be used to create
406   *                        the new resource instance.
407   * @param includeReadOnlyAttributes whether read-only attributes should be
408   *                                  included in the partial resource. If this
409   *                                  is {@code false}, these attributes will be
410   *                                  stripped out.
411   * @return The partial resource with the modifications that maybe sent in
412   *         a PATCH request.
413   * @throws InvalidResourceException If an error occurs.
414   */
415  public R toPartialResource(final ResourceFactory<R> resourceFactory,
416                             final boolean includeReadOnlyAttributes)
417      throws InvalidResourceException
418  {
419    SCIMObject scimObject = new SCIMObject();
420    if(attributesToDelete != null && !attributesToDelete.isEmpty())
421    {
422      SCIMAttributeValue[] values =
423              new SCIMAttributeValue[attributesToDelete.size()];
424      for(int i = 0; i < attributesToDelete.size(); i++)
425      {
426        values[i] = SCIMAttributeValue.createStringValue(
427                            attributesToDelete.get(i));
428      }
429
430      AttributeDescriptor subDescriptor =
431              CoreSchema.META_DESCRIPTOR.getSubAttribute("attributes");
432
433      SCIMAttribute attributes = SCIMAttribute.create(subDescriptor, values);
434
435      SCIMAttribute meta = SCIMAttribute.create(
436              CoreSchema.META_DESCRIPTOR,
437              SCIMAttributeValue.createComplexValue(attributes));
438
439      scimObject.setAttribute(meta);
440    }
441
442    if(attributesToUpdate != null)
443    {
444      for(SCIMAttribute attr : attributesToUpdate)
445      {
446        if(!attr.getAttributeDescriptor().isReadOnly() ||
447                includeReadOnlyAttributes)
448        {
449          scimObject.setAttribute(attr);
450        }
451      }
452    }
453
454    return resourceFactory.createResource(resourceDescriptor, scimObject);
455  }
456
457  /**
458   * Generates a diff with modifications that can be applied to the source
459   * resource in order to make it match the target resource.
460   *
461   * @param <R> The type of the source and target resource instances.
462   * @param partialResource The partial resource containing the PATCH
463   *                        modifications from which to generate the diff.
464   * @param includeReadOnlyAttributes whether read-only attributes should be
465   *                                  included in the Diff. If this is
466   *                                  {@code false}, these attributes will be
467   *                                  stripped out.
468   * @return A diff with modifications that can be applied to the source
469   *         resource in order to make it match the target resource.
470   */
471  public static <R extends BaseResource> Diff<R> fromPartialResource(
472           final R partialResource, final boolean includeReadOnlyAttributes)
473  {
474    final SCIMObject scimObject =
475            new SCIMObject(partialResource.getScimObject());
476    final Set<String> attributesToDelete = new HashSet<String>();
477    final List<SCIMAttribute> attributesToUpdate =
478            new ArrayList<SCIMAttribute>(10);
479
480    SCIMAttribute metaAttr = scimObject.getAttribute(
481            SCIMConstants.SCHEMA_URI_CORE, "meta");
482
483    if(metaAttr != null)
484    {
485      SCIMAttribute attributesAttr =
486              metaAttr.getValue().getAttribute("attributes");
487
488      if(attributesAttr != null)
489      {
490        for(SCIMAttributeValue attrPath : attributesAttr.getValues())
491        {
492          attributesToDelete.add(attrPath.getStringValue());
493        }
494      }
495    }
496
497    scimObject.removeAttribute(SCIMConstants.SCHEMA_URI_CORE, "meta");
498
499    for(String schema : scimObject.getSchemas())
500    {
501      for(SCIMAttribute attr : scimObject.getAttributes(schema))
502      {
503        if(!attr.getAttributeDescriptor().isReadOnly() ||
504                includeReadOnlyAttributes)
505        {
506          attributesToUpdate.add(attr);
507        }
508      }
509    }
510
511    return new Diff<R>(partialResource.getResourceDescriptor(),
512        Collections.unmodifiableList(new ArrayList<String>(attributesToDelete)),
513        Collections.unmodifiableList(attributesToUpdate));
514  }
515
516  /**
517   * Generates a diff with modifications that can be applied to the source
518   * resource in order to make it match the target resource.
519   *
520   * @param <R>    The type of the source and target resource instances.
521   * @param source The source resource for which the set of modifications should
522   *               be generated.
523   * @param target The target resource, which is what the source resource should
524   *               look like if the returned modifications are applied.
525   * @param attributes The set of attributes to be compared in standard
526   *                   attribute notation (ie. name.givenName). If this is
527   *                   {@code null} or empty, then all attributes will be
528   *                   compared.
529   * @return A diff with modifications that can be applied to the source
530   *         resource in order to make it match the target resource.
531   */
532  public static <R extends BaseResource> Diff<R> generate(
533      final R source, final R target, final String... attributes)
534  {
535    final SCIMObject sourceObject = source.getScimObject();
536    final SCIMObject targetObject = target.getScimObject();
537
538    HashMap<String, HashMap<String, HashSet<String>>> compareAttrs = null;
539    if ((attributes != null) && (attributes.length > 0))
540    {
541      compareAttrs = new HashMap<String, HashMap<String, HashSet<String>>>();
542      for (final String s : attributes)
543      {
544        final AttributePath path = AttributePath.parse(s);
545        final String schema = toLowerCase(path.getAttributeSchema());
546        final String attrName = toLowerCase(path.getAttributeName());
547        final String subAttrName = path.getSubAttributeName() == null ? null :
548            toLowerCase(path.getSubAttributeName());
549
550        HashMap<String, HashSet<String>> schemaAttrs = compareAttrs.get(schema);
551        if(schemaAttrs == null)
552        {
553          schemaAttrs = new HashMap<String, HashSet<String>>();
554          compareAttrs.put(schema, schemaAttrs);
555        }
556        HashSet<String> subAttrs = schemaAttrs.get(attrName);
557        if(subAttrs == null)
558        {
559          subAttrs = new HashSet<String>();
560          schemaAttrs.put(attrName, subAttrs);
561        }
562        if(subAttrName != null)
563        {
564          subAttrs.add(subAttrName);
565        }
566      }
567    }
568
569    final SCIMObject sourceOnlyAttrs = new SCIMObject();
570    final SCIMObject targetOnlyAttrs = new SCIMObject();
571    final SCIMObject commonAttrs = new SCIMObject();
572
573    for (final String schema : sourceObject.getSchemas())
574    {
575      for (final SCIMAttribute attribute : sourceObject.getAttributes(schema))
576      {
577        if (!shouldProcess(compareAttrs, attribute, null))
578        {
579          continue;
580        }
581
582        sourceOnlyAttrs.setAttribute(attribute);
583        commonAttrs.setAttribute(attribute);
584      }
585    }
586
587    for (final String schema : targetObject.getSchemas())
588    {
589      for (final SCIMAttribute attribute : targetObject.getAttributes(schema))
590      {
591        if (!shouldProcess(compareAttrs, attribute, null))
592        {
593          continue;
594        }
595
596        if (!sourceOnlyAttrs.removeAttribute(
597            attribute.getSchema(), attribute.getName()))
598        {
599          // It wasn't in the set of source attributes, so it must be a
600          // target-only attribute.
601          targetOnlyAttrs.setAttribute(attribute);
602        }
603      }
604    }
605
606    for (final String schema : sourceOnlyAttrs.getSchemas())
607    {
608      for (final SCIMAttribute attribute :
609          sourceOnlyAttrs.getAttributes(schema))
610      {
611        commonAttrs.removeAttribute(attribute.getSchema(), attribute.getName());
612      }
613    }
614
615    final Set<String> attributesToDelete = new HashSet<String>();
616    final List<SCIMAttribute> attributesToUpdate =
617        new ArrayList<SCIMAttribute>(10);
618
619    // Delete all attributes that are only in the source object
620    for (final String schema : sourceOnlyAttrs.getSchemas())
621    {
622      for (final SCIMAttribute sourceAttribute :
623          sourceOnlyAttrs.getAttributes(schema))
624      {
625        deleteAttribute(compareAttrs, attributesToDelete, sourceAttribute);
626      }
627    }
628
629    // Add all attributes that are only in the target object
630    for (final String schema : targetOnlyAttrs.getSchemas())
631    {
632      for (final SCIMAttribute targetAttribute :
633          targetOnlyAttrs.getAttributes(schema))
634      {
635        if (targetAttribute.getAttributeDescriptor().isMultiValued())
636        {
637          ArrayList<SCIMAttributeValue> targetValues =
638              new ArrayList<SCIMAttributeValue>(
639                  targetAttribute.getValues().length);
640          for (SCIMAttributeValue targetValue : targetAttribute.getValues())
641          {
642            Map<String, SCIMAttribute> subAttrs =
643                filterSubAttributes(compareAttrs, targetAttribute,
644                    targetValue);
645            if(!subAttrs.isEmpty())
646            {
647              targetValues.add(
648                  SCIMAttributeValue.createComplexValue(subAttrs.values()));
649            }
650          }
651          if(!targetValues.isEmpty())
652          {
653            attributesToUpdate.add(SCIMAttribute.create(
654                targetAttribute.getAttributeDescriptor(), targetValues.toArray(
655                new SCIMAttributeValue[targetValues.size()])));
656          }
657        }
658        else if(targetAttribute.getValue().isComplex())
659        {
660          Map<String, SCIMAttribute> subAttrs =
661              filterSubAttributes(compareAttrs, targetAttribute,
662                  targetAttribute.getValue());
663          if(!subAttrs.isEmpty())
664          {
665            attributesToUpdate.add(
666                SCIMAttribute.create(targetAttribute.getAttributeDescriptor(),
667                    SCIMAttributeValue.createComplexValue(subAttrs.values())));
668          }
669        }
670        else
671        {
672          attributesToUpdate.add(targetAttribute);
673        }
674      }
675    }
676
677    // Add all common attributes with different values
678    for (final String schema : commonAttrs.getSchemas())
679    {
680      for (final SCIMAttribute sourceAttribute :
681          commonAttrs.getAttributes(schema))
682      {
683        SCIMAttribute targetAttribute =
684            targetObject.getAttribute(sourceAttribute.getSchema(),
685                sourceAttribute.getName());
686        if (sourceAttribute.equals(targetAttribute))
687        {
688          continue;
689        }
690
691        if(sourceAttribute.getAttributeDescriptor().isMultiValued())
692        {
693          Set<SCIMAttributeValue> sourceValues =
694              new LinkedHashSet<SCIMAttributeValue>(
695                  sourceAttribute.getValues().length);
696          Set<SCIMAttributeValue> targetValues =
697              new LinkedHashSet<SCIMAttributeValue>(
698                  targetAttribute.getValues().length);
699          Collections.addAll(sourceValues, sourceAttribute.getValues());
700
701          for (SCIMAttributeValue v : targetAttribute.getValues())
702          {
703            if (!sourceValues.remove(v))
704            {
705              // This value could be an added or updated value
706              // TODO: Support matching on value sub-attribute if possible?
707              targetValues.add(v);
708            }
709          }
710
711          if(sourceValues.size() == sourceAttribute.getValues().length)
712          {
713            // All source values seem to have been deleted. Just delete the
714            // attribute instead of listing all delete values.
715            deleteAttribute(compareAttrs, attributesToDelete, sourceAttribute);
716            sourceValues = Collections.emptySet();
717          }
718
719          ArrayList<SCIMAttributeValue> patchValues =
720              new ArrayList<SCIMAttributeValue>(
721                  sourceValues.size() + targetValues.size());
722          for (SCIMAttributeValue sourceValue : sourceValues)
723          {
724            Map<String, SCIMAttribute> subAttrs =
725                filterSubAttributes(compareAttrs, sourceAttribute, sourceValue);
726            if(!subAttrs.isEmpty())
727            {
728              SCIMAttribute operationAttr;
729              try
730              {
731                operationAttr = SCIMAttribute.create(
732                    sourceAttribute.getAttributeDescriptor().getSubAttribute(
733                        "operation"),
734                    SCIMAttributeValue.createStringValue("delete"));
735              }
736              catch (InvalidResourceException e)
737              {
738                // This should never happen
739                throw new IllegalStateException(e);
740              }
741              subAttrs.put(toLowerCase(operationAttr.getName()), operationAttr);
742              patchValues.add(SCIMAttributeValue.createComplexValue(
743                  subAttrs.values()));
744            }
745          }
746          for (SCIMAttributeValue targetValue : targetValues)
747          {
748            // Add any new or updated target sub-attributes
749            Map<String, SCIMAttribute> subAttrs =
750                filterSubAttributes(compareAttrs, targetAttribute, targetValue);
751            if(!subAttrs.isEmpty())
752            {
753              patchValues.add(SCIMAttributeValue.createComplexValue(
754                              subAttrs.values()));
755            }
756          }
757          if(!patchValues.isEmpty())
758          {
759            attributesToUpdate.add(SCIMAttribute.create(
760                sourceAttribute.getAttributeDescriptor(), patchValues.toArray(
761                new SCIMAttributeValue[patchValues.size()])));
762          }
763        }
764        else if(sourceAttribute.getValue().isComplex())
765        {
766          // Remove any source only sub-attributes
767          SCIMAttributeValue sourceAttributeValue =
768              sourceAttribute.getValue();
769          SCIMAttributeValue targetAttributeValue =
770              targetAttribute.getValue();
771          for (final Map.Entry<String, SCIMAttribute> e :
772              filterSubAttributes(compareAttrs, sourceAttribute,
773                  sourceAttributeValue).entrySet())
774          {
775            if(!targetAttributeValue.hasAttribute(e.getKey()))
776            {
777              final AttributePath path =
778                  new AttributePath(sourceAttribute.getSchema(),
779                      sourceAttribute.getName(), e.getValue().getName());
780              attributesToDelete.add(path.toString());
781            }
782          }
783
784          // Add any new or updated target sub-attributes
785          Map<String, SCIMAttribute> targetSubAttrs =
786              filterSubAttributes(compareAttrs, targetAttribute,
787                  targetAttributeValue);
788          final Iterator<Map.Entry<String, SCIMAttribute>> targetIterator =
789              targetSubAttrs.entrySet().iterator();
790          while(targetIterator.hasNext())
791          {
792            Map.Entry<String, SCIMAttribute> e = targetIterator.next();
793            SCIMAttribute sourceSubAttr =
794                sourceAttributeValue.getAttribute(e.getKey());
795            if(sourceSubAttr != null && sourceSubAttr.equals(e.getValue()))
796            {
797              // This sub-attribute is the same so do not include it in the
798              // patch.
799              targetIterator.remove();
800            }
801          }
802          if(!targetSubAttrs.isEmpty())
803          {
804            attributesToUpdate.add(SCIMAttribute.create(
805                targetAttribute.getAttributeDescriptor(),
806                SCIMAttributeValue.createComplexValue(
807                    targetSubAttrs.values())));
808          }
809        }
810        else
811        {
812          attributesToUpdate.add(targetAttribute);
813        }
814      }
815    }
816
817    return new Diff<R>(source.getResourceDescriptor(),
818        Collections.unmodifiableList(new ArrayList<String>(attributesToDelete)),
819        Collections.unmodifiableList(attributesToUpdate));
820  }
821
822  /**
823   * Utility method to determine if an attribute should be processed when
824   * generating the modifications.
825   *
826   * @param compareAttrs The map of attributes to be compared.
827   * @param attribute The attribute to consider.
828   * @param subAttribute The sub-attribute to consider or {@code null} if
829   *                     not available.
830   * @return {@code true} if the attribute should be processed or
831   *         {@code false} otherwise.
832   */
833  private static boolean shouldProcess(
834      final HashMap<String, HashMap<String, HashSet<String>>> compareAttrs,
835      final SCIMAttribute attribute, final SCIMAttribute subAttribute)
836  {
837    if(compareAttrs == null)
838    {
839      return true;
840    }
841
842    final HashMap<String, HashSet<String>> schemaAttrs =
843        compareAttrs.get(toLowerCase(attribute.getSchema()));
844
845    if(schemaAttrs == null)
846    {
847      return false;
848    }
849
850    final HashSet<String> subAttrs = schemaAttrs.get(toLowerCase(
851        attribute.getName()));
852
853    if(subAttribute == null ||
854        attribute.getAttributeDescriptor().getDataType() !=
855            AttributeDescriptor.DataType.COMPLEX)
856    {
857      return subAttrs != null;
858    }
859    else
860    {
861      return subAttrs != null &&
862          subAttrs.contains(toLowerCase(subAttribute.getName()));
863    }
864  }
865
866  /**
867   * Utility method to filter sub-attributes down to only those that should
868   * be processed when generating the modifications.
869   *
870   * @param compareAttrs The map of attributes to be compared.
871   * @param attribute The attribute to consider.
872   * @param value     The complex SCIMAttributeValue to filter
873   * @return A map of sub-attributes that should be included in the diff.
874   */
875  private static Map<String, SCIMAttribute> filterSubAttributes(
876      final HashMap<String, HashMap<String, HashSet<String>>> compareAttrs,
877      final SCIMAttribute attribute, final SCIMAttributeValue value)
878  {
879    Map<String, SCIMAttribute> filteredSubAttributes =
880        new LinkedHashMap<String, SCIMAttribute>(
881            value.getAttributes());
882    Iterator<Map.Entry<String, SCIMAttribute>> subAttrsIterator =
883        filteredSubAttributes.entrySet().iterator();
884    while(subAttrsIterator.hasNext())
885    {
886      Map.Entry<String, SCIMAttribute> e = subAttrsIterator.next();
887      if(!shouldProcess(compareAttrs, attribute, e.getValue()))
888      {
889        subAttrsIterator.remove();
890      }
891    }
892
893    return filteredSubAttributes;
894  }
895
896  /**
897   * Utility method to add an attribute and all its sub-attributes if
898   * applicable to the attributesToDelete set.
899   *
900   * @param compareAttrs The map of attributes to be compared.
901   * @param attributesToDelete The list of attributes to delete to append.
902   * @param attribute The attribute to delete.
903   */
904  private static void deleteAttribute(
905      final HashMap<String, HashMap<String, HashSet<String>>> compareAttrs,
906      final Set<String> attributesToDelete, final SCIMAttribute attribute)
907  {
908    if(attribute.getAttributeDescriptor().getDataType() ==
909        AttributeDescriptor.DataType.COMPLEX)
910    {
911      if(attribute.getAttributeDescriptor().isMultiValued())
912      {
913        for(SCIMAttributeValue sourceValue : attribute.getValues())
914        {
915          for(Map.Entry<String, SCIMAttribute> e :
916              filterSubAttributes(compareAttrs, attribute,
917                  sourceValue).entrySet())
918          {
919            // Skip normative sub-attributes for multi-valued attributes
920            if(e.getKey().equals("type") ||
921                e.getKey().equals("primary") ||
922                e.getKey().equals("operation") ||
923                e.getKey().equals("display") ||
924                e.getKey().equals("value"))
925            {
926              continue;
927            }
928
929            final AttributePath path =
930                new AttributePath(attribute.getSchema(),
931                    attribute.getName(), e.getKey());
932            attributesToDelete.add(path.toString());
933          }
934        }
935      }
936      else
937      {
938        for(Map.Entry<String, SCIMAttribute> e :
939            filterSubAttributes(compareAttrs, attribute,
940                attribute.getValue()).entrySet())
941        {
942          final AttributePath path =
943              new AttributePath(attribute.getSchema(),
944                  attribute.getName(), e.getKey());
945          attributesToDelete.add(path.toString());
946        }
947      }
948    }
949    else
950    {
951      final AttributePath path =
952          new AttributePath(attribute.getSchema(),
953              attribute.getName(),
954              null);
955      attributesToDelete.add(path.toString());
956    }
957  }
958
959}