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}