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}