001/*
002 * Copyright 2013-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.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    if (!includeReadOnlyAttributes) {
498      scimObject.removeAttribute(SCIMConstants.SCHEMA_URI_CORE, "meta");
499    }
500
501    for(String schema : scimObject.getSchemas())
502    {
503      for(SCIMAttribute attr : scimObject.getAttributes(schema))
504      {
505        if(!attr.getAttributeDescriptor().isReadOnly() ||
506                includeReadOnlyAttributes)
507        {
508          attributesToUpdate.add(attr);
509        }
510      }
511    }
512
513    return new Diff<R>(partialResource.getResourceDescriptor(),
514        Collections.unmodifiableList(new ArrayList<String>(attributesToDelete)),
515        Collections.unmodifiableList(attributesToUpdate));
516  }
517
518  /**
519   * Generates a diff with modifications that can be applied to the source
520   * resource in order to make it match the target resource.
521   *
522   * @param <R>    The type of the source and target resource instances.
523   * @param source The source resource for which the set of modifications should
524   *               be generated.
525   * @param target The target resource, which is what the source resource should
526   *               look like if the returned modifications are applied.
527   * @param attributes The set of attributes to be compared in standard
528   *                   attribute notation (ie. name.givenName). If this is
529   *                   {@code null} or empty, then all attributes will be
530   *                   compared.
531   * @return A diff with modifications that can be applied to the source
532   *         resource in order to make it match the target resource.
533   */
534  public static <R extends BaseResource> Diff<R> generate(
535      final R source, final R target, final String... attributes)
536  {
537    final SCIMObject sourceObject = source.getScimObject();
538    final SCIMObject targetObject = target.getScimObject();
539
540    HashMap<String, HashMap<String, HashSet<String>>> compareAttrs = null;
541    if ((attributes != null) && (attributes.length > 0))
542    {
543      compareAttrs = new HashMap<String, HashMap<String, HashSet<String>>>();
544      for (final String s : attributes)
545      {
546        final AttributePath path = AttributePath.parse(s);
547        final String schema = toLowerCase(path.getAttributeSchema());
548        final String attrName = toLowerCase(path.getAttributeName());
549        final String subAttrName = path.getSubAttributeName() == null ? null :
550            toLowerCase(path.getSubAttributeName());
551
552        HashMap<String, HashSet<String>> schemaAttrs = compareAttrs.get(schema);
553        if(schemaAttrs == null)
554        {
555          schemaAttrs = new HashMap<String, HashSet<String>>();
556          compareAttrs.put(schema, schemaAttrs);
557        }
558        HashSet<String> subAttrs = schemaAttrs.get(attrName);
559        if(subAttrs == null)
560        {
561          subAttrs = new HashSet<String>();
562          schemaAttrs.put(attrName, subAttrs);
563        }
564        if(subAttrName != null)
565        {
566          subAttrs.add(subAttrName);
567        }
568      }
569    }
570
571    final SCIMObject sourceOnlyAttrs = new SCIMObject();
572    final SCIMObject targetOnlyAttrs = new SCIMObject();
573    final SCIMObject commonAttrs = new SCIMObject();
574
575    for (final String schema : sourceObject.getSchemas())
576    {
577      for (final SCIMAttribute attribute : sourceObject.getAttributes(schema))
578      {
579        if (!shouldProcess(compareAttrs, attribute, null))
580        {
581          continue;
582        }
583
584        sourceOnlyAttrs.setAttribute(attribute);
585        commonAttrs.setAttribute(attribute);
586      }
587    }
588
589    for (final String schema : targetObject.getSchemas())
590    {
591      for (final SCIMAttribute attribute : targetObject.getAttributes(schema))
592      {
593        if (!shouldProcess(compareAttrs, attribute, null))
594        {
595          continue;
596        }
597
598        if (!sourceOnlyAttrs.removeAttribute(
599            attribute.getSchema(), attribute.getName()))
600        {
601          // It wasn't in the set of source attributes, so it must be a
602          // target-only attribute.
603          targetOnlyAttrs.setAttribute(attribute);
604        }
605      }
606    }
607
608    for (final String schema : sourceOnlyAttrs.getSchemas())
609    {
610      for (final SCIMAttribute attribute :
611          sourceOnlyAttrs.getAttributes(schema))
612      {
613        commonAttrs.removeAttribute(attribute.getSchema(), attribute.getName());
614      }
615    }
616
617    final Set<String> attributesToDelete = new HashSet<String>();
618    final List<SCIMAttribute> attributesToUpdate =
619        new ArrayList<SCIMAttribute>(10);
620
621    // Delete all attributes that are only in the source object
622    for (final String schema : sourceOnlyAttrs.getSchemas())
623    {
624      for (final SCIMAttribute sourceAttribute :
625          sourceOnlyAttrs.getAttributes(schema))
626      {
627        deleteAttribute(compareAttrs, attributesToDelete, sourceAttribute);
628      }
629    }
630
631    // Add all attributes that are only in the target object
632    for (final String schema : targetOnlyAttrs.getSchemas())
633    {
634      for (final SCIMAttribute targetAttribute :
635          targetOnlyAttrs.getAttributes(schema))
636      {
637        if (targetAttribute.getAttributeDescriptor().isMultiValued())
638        {
639          ArrayList<SCIMAttributeValue> targetValues =
640              new ArrayList<SCIMAttributeValue>(
641                  targetAttribute.getValues().length);
642          for (SCIMAttributeValue targetValue : targetAttribute.getValues())
643          {
644            Map<String, SCIMAttribute> subAttrs =
645                filterSubAttributes(compareAttrs, targetAttribute,
646                    targetValue);
647            if(!subAttrs.isEmpty())
648            {
649              targetValues.add(
650                  SCIMAttributeValue.createComplexValue(subAttrs.values()));
651            }
652          }
653          if(!targetValues.isEmpty())
654          {
655            attributesToUpdate.add(SCIMAttribute.create(
656                targetAttribute.getAttributeDescriptor(), targetValues.toArray(
657                new SCIMAttributeValue[targetValues.size()])));
658          }
659        }
660        else if(targetAttribute.getValue().isComplex())
661        {
662          Map<String, SCIMAttribute> subAttrs =
663              filterSubAttributes(compareAttrs, targetAttribute,
664                  targetAttribute.getValue());
665          if(!subAttrs.isEmpty())
666          {
667            attributesToUpdate.add(
668                SCIMAttribute.create(targetAttribute.getAttributeDescriptor(),
669                    SCIMAttributeValue.createComplexValue(subAttrs.values())));
670          }
671        }
672        else
673        {
674          attributesToUpdate.add(targetAttribute);
675        }
676      }
677    }
678
679    // Add all common attributes with different values
680    for (final String schema : commonAttrs.getSchemas())
681    {
682      for (final SCIMAttribute sourceAttribute :
683          commonAttrs.getAttributes(schema))
684      {
685        SCIMAttribute targetAttribute =
686            targetObject.getAttribute(sourceAttribute.getSchema(),
687                sourceAttribute.getName());
688        if (sourceAttribute.equals(targetAttribute))
689        {
690          continue;
691        }
692
693        if(sourceAttribute.getAttributeDescriptor().isMultiValued())
694        {
695          Set<SCIMAttributeValue> sourceValues =
696              new LinkedHashSet<SCIMAttributeValue>(
697                  sourceAttribute.getValues().length);
698          Set<SCIMAttributeValue> targetValues =
699              new LinkedHashSet<SCIMAttributeValue>(
700                  targetAttribute.getValues().length);
701          Collections.addAll(sourceValues, sourceAttribute.getValues());
702
703          for (SCIMAttributeValue v : targetAttribute.getValues())
704          {
705            if (!sourceValues.remove(v))
706            {
707              // This value could be an added or updated value
708              // TODO: Support matching on value sub-attribute if possible?
709              targetValues.add(v);
710            }
711          }
712
713          if(sourceValues.size() == sourceAttribute.getValues().length)
714          {
715            // All source values seem to have been deleted. Just delete the
716            // attribute instead of listing all delete values.
717            deleteAttribute(compareAttrs, attributesToDelete, sourceAttribute);
718            sourceValues = Collections.emptySet();
719          }
720
721          ArrayList<SCIMAttributeValue> patchValues =
722              new ArrayList<SCIMAttributeValue>(
723                  sourceValues.size() + targetValues.size());
724          for (SCIMAttributeValue sourceValue : sourceValues)
725          {
726            Map<String, SCIMAttribute> subAttrs =
727                filterSubAttributes(compareAttrs, sourceAttribute, sourceValue);
728            if(!subAttrs.isEmpty())
729            {
730              SCIMAttribute operationAttr;
731              try
732              {
733                operationAttr = SCIMAttribute.create(
734                    sourceAttribute.getAttributeDescriptor().getSubAttribute(
735                        "operation"),
736                    SCIMAttributeValue.createStringValue("delete"));
737              }
738              catch (InvalidResourceException e)
739              {
740                // This should never happen
741                throw new IllegalStateException(e);
742              }
743              subAttrs.put(toLowerCase(operationAttr.getName()), operationAttr);
744              patchValues.add(SCIMAttributeValue.createComplexValue(
745                  subAttrs.values()));
746            }
747          }
748          for (SCIMAttributeValue targetValue : targetValues)
749          {
750            // Add any new or updated target sub-attributes
751            Map<String, SCIMAttribute> subAttrs =
752                filterSubAttributes(compareAttrs, targetAttribute, targetValue);
753            if(!subAttrs.isEmpty())
754            {
755              patchValues.add(SCIMAttributeValue.createComplexValue(
756                              subAttrs.values()));
757            }
758          }
759          if(!patchValues.isEmpty())
760          {
761            attributesToUpdate.add(SCIMAttribute.create(
762                sourceAttribute.getAttributeDescriptor(), patchValues.toArray(
763                new SCIMAttributeValue[patchValues.size()])));
764          }
765        }
766        else if(sourceAttribute.getValue().isComplex())
767        {
768          // Remove any source only sub-attributes
769          SCIMAttributeValue sourceAttributeValue =
770              sourceAttribute.getValue();
771          SCIMAttributeValue targetAttributeValue =
772              targetAttribute.getValue();
773          for (final Map.Entry<String, SCIMAttribute> e :
774              filterSubAttributes(compareAttrs, sourceAttribute,
775                  sourceAttributeValue).entrySet())
776          {
777            if(!targetAttributeValue.hasAttribute(e.getKey()))
778            {
779              final AttributePath path =
780                  new AttributePath(sourceAttribute.getSchema(),
781                      sourceAttribute.getName(), e.getValue().getName());
782              attributesToDelete.add(path.toString());
783            }
784          }
785
786          // Add any new or updated target sub-attributes
787          Map<String, SCIMAttribute> targetSubAttrs =
788              filterSubAttributes(compareAttrs, targetAttribute,
789                  targetAttributeValue);
790          final Iterator<Map.Entry<String, SCIMAttribute>> targetIterator =
791              targetSubAttrs.entrySet().iterator();
792          while(targetIterator.hasNext())
793          {
794            Map.Entry<String, SCIMAttribute> e = targetIterator.next();
795            SCIMAttribute sourceSubAttr =
796                sourceAttributeValue.getAttribute(e.getKey());
797            if(sourceSubAttr != null && sourceSubAttr.equals(e.getValue()))
798            {
799              // This sub-attribute is the same so do not include it in the
800              // patch.
801              targetIterator.remove();
802            }
803          }
804          if(!targetSubAttrs.isEmpty())
805          {
806            attributesToUpdate.add(SCIMAttribute.create(
807                targetAttribute.getAttributeDescriptor(),
808                SCIMAttributeValue.createComplexValue(
809                    targetSubAttrs.values())));
810          }
811        }
812        else
813        {
814          attributesToUpdate.add(targetAttribute);
815        }
816      }
817    }
818
819    return new Diff<R>(source.getResourceDescriptor(),
820        Collections.unmodifiableList(new ArrayList<String>(attributesToDelete)),
821        Collections.unmodifiableList(attributesToUpdate));
822  }
823
824  /**
825   * Utility method to determine if an attribute should be processed when
826   * generating the modifications.
827   *
828   * @param compareAttrs The map of attributes to be compared.
829   * @param attribute The attribute to consider.
830   * @param subAttribute The sub-attribute to consider or {@code null} if
831   *                     not available.
832   * @return {@code true} if the attribute should be processed or
833   *         {@code false} otherwise.
834   */
835  private static boolean shouldProcess(
836      final HashMap<String, HashMap<String, HashSet<String>>> compareAttrs,
837      final SCIMAttribute attribute, final SCIMAttribute subAttribute)
838  {
839    if(compareAttrs == null)
840    {
841      return true;
842    }
843
844    final HashMap<String, HashSet<String>> schemaAttrs =
845        compareAttrs.get(toLowerCase(attribute.getSchema()));
846
847    if(schemaAttrs == null)
848    {
849      return false;
850    }
851
852    final HashSet<String> subAttrs = schemaAttrs.get(toLowerCase(
853        attribute.getName()));
854
855    return subAttrs != null && (
856        !(subAttribute != null && !subAttrs.isEmpty()) ||
857            subAttrs.contains(toLowerCase(subAttribute.getName())));
858  }
859
860  /**
861   * Utility method to filter sub-attributes down to only those that should
862   * be processed when generating the modifications.
863   *
864   * @param compareAttrs The map of attributes to be compared.
865   * @param attribute The attribute to consider.
866   * @param value     The complex SCIMAttributeValue to filter
867   * @return A map of sub-attributes that should be included in the diff.
868   */
869  private static Map<String, SCIMAttribute> filterSubAttributes(
870      final HashMap<String, HashMap<String, HashSet<String>>> compareAttrs,
871      final SCIMAttribute attribute, final SCIMAttributeValue value)
872  {
873    Map<String, SCIMAttribute> filteredSubAttributes =
874        new LinkedHashMap<String, SCIMAttribute>(
875            value.getAttributes());
876    Iterator<Map.Entry<String, SCIMAttribute>> subAttrsIterator =
877        filteredSubAttributes.entrySet().iterator();
878    while(subAttrsIterator.hasNext())
879    {
880      Map.Entry<String, SCIMAttribute> e = subAttrsIterator.next();
881      if(!shouldProcess(compareAttrs, attribute, e.getValue()))
882      {
883        subAttrsIterator.remove();
884      }
885    }
886
887    return filteredSubAttributes;
888  }
889
890  /**
891   * Utility method to add an attribute and all its sub-attributes if
892   * applicable to the attributesToDelete set.
893   *
894   * @param compareAttrs The map of attributes to be compared.
895   * @param attributesToDelete The list of attributes to delete to append.
896   * @param attribute The attribute to delete.
897   */
898  private static void deleteAttribute(
899      final HashMap<String, HashMap<String, HashSet<String>>> compareAttrs,
900      final Set<String> attributesToDelete, final SCIMAttribute attribute)
901  {
902    if(attribute.getAttributeDescriptor().isMultiValued())
903    {
904      // Technically, all multi-valued attributes are complex since they may
905      // have sub-attributes.
906      Set<String> subAttributes = new HashSet<String>();
907      for(SCIMAttributeValue sourceValue : attribute.getValues())
908      {
909        if(sourceValue.isComplex())
910        {
911          for(Map.Entry<String, SCIMAttribute> e :
912              filterSubAttributes(compareAttrs, attribute,
913                  sourceValue).entrySet())
914          {
915            // Skip non-significant normative sub-attributes
916            if(e.getKey().equals("type") ||
917                e.getKey().equals("primary") ||
918                e.getKey().equals("operation") ||
919                e.getKey().equals("display"))
920            {
921              continue;
922            }
923
924            final AttributePath path =
925                new AttributePath(attribute.getSchema(),
926                    attribute.getName(), e.getKey());
927            subAttributes.add(path.toString());
928          }
929        }
930        else
931        {
932          // There are no sub-attributes for this attribute, which is
933          // technically not correct. Just delete the whole attribute
934          final AttributePath path =
935              new AttributePath(attribute.getSchema(),
936                  attribute.getName(),
937                  null);
938          subAttributes.clear();
939          attributesToDelete.add(path.toString());
940          break;
941        }
942      }
943      attributesToDelete.addAll(subAttributes);
944    }
945    else if(attribute.getAttributeDescriptor().getDataType() ==
946        AttributeDescriptor.DataType.COMPLEX)
947    {
948      for(Map.Entry<String, SCIMAttribute> e :
949          filterSubAttributes(compareAttrs, attribute,
950              attribute.getValue()).entrySet())
951      {
952        final AttributePath path =
953            new AttributePath(attribute.getSchema(),
954                attribute.getName(), e.getKey());
955        attributesToDelete.add(path.toString());
956      }
957    }
958    else
959    {
960      final AttributePath path =
961          new AttributePath(attribute.getSchema(),
962              attribute.getName(),
963              null);
964      attributesToDelete.add(path.toString());
965    }
966  }
967
968}