001/*- 002 * #%L 003 * HAPI FHIR Storage api 004 * %% 005 * Copyright (C) 2014 - 2024 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.jpa.patch; 021 022import ca.uhn.fhir.context.BaseRuntimeChildDefinition; 023import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; 024import ca.uhn.fhir.context.BaseRuntimeElementDefinition; 025import ca.uhn.fhir.context.FhirContext; 026import ca.uhn.fhir.i18n.Msg; 027import ca.uhn.fhir.parser.path.EncodeContextPath; 028import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 029import ca.uhn.fhir.util.IModelVisitor2; 030import ca.uhn.fhir.util.ParametersUtil; 031import jakarta.annotation.Nonnull; 032import jakarta.annotation.Nullable; 033import org.apache.commons.lang3.StringUtils; 034import org.apache.commons.lang3.Validate; 035import org.hl7.fhir.instance.model.api.IBase; 036import org.hl7.fhir.instance.model.api.IBaseEnumeration; 037import org.hl7.fhir.instance.model.api.IBaseParameters; 038import org.hl7.fhir.instance.model.api.IBaseResource; 039import org.hl7.fhir.instance.model.api.IIdType; 040import org.hl7.fhir.instance.model.api.IPrimitiveType; 041import org.hl7.fhir.utilities.xhtml.XhtmlNode; 042 043import java.util.ArrayList; 044import java.util.Collections; 045import java.util.HashSet; 046import java.util.List; 047import java.util.Objects; 048import java.util.Optional; 049import java.util.Set; 050 051import static org.apache.commons.lang3.StringUtils.defaultString; 052 053public class FhirPatch { 054 055 public static final String OPERATION_ADD = "add"; 056 public static final String OPERATION_DELETE = "delete"; 057 public static final String OPERATION_INSERT = "insert"; 058 public static final String OPERATION_MOVE = "move"; 059 public static final String OPERATION_REPLACE = "replace"; 060 public static final String PARAMETER_DESTINATION = "destination"; 061 public static final String PARAMETER_INDEX = "index"; 062 public static final String PARAMETER_NAME = "name"; 063 public static final String PARAMETER_OPERATION = "operation"; 064 public static final String PARAMETER_PATH = "path"; 065 public static final String PARAMETER_SOURCE = "source"; 066 public static final String PARAMETER_TYPE = "type"; 067 public static final String PARAMETER_VALUE = "value"; 068 069 private final FhirContext myContext; 070 private boolean myIncludePreviousValueInDiff; 071 private Set<EncodeContextPath> myIgnorePaths = Collections.emptySet(); 072 073 public FhirPatch(FhirContext theContext) { 074 myContext = theContext; 075 } 076 077 /** 078 * Adds a path element that will not be included in generated diffs. Values can take the form 079 * <code>ResourceName.fieldName.fieldName</code> and wildcards are supported, such 080 * as <code>*.meta</code>. 081 */ 082 public void addIgnorePath(String theIgnorePath) { 083 Validate.notBlank(theIgnorePath, "theIgnorePath must not be null or empty"); 084 085 if (myIgnorePaths.isEmpty()) { 086 myIgnorePaths = new HashSet<>(); 087 } 088 myIgnorePaths.add(new EncodeContextPath(theIgnorePath)); 089 } 090 091 public void setIncludePreviousValueInDiff(boolean theIncludePreviousValueInDiff) { 092 myIncludePreviousValueInDiff = theIncludePreviousValueInDiff; 093 } 094 095 public void apply(IBaseResource theResource, IBaseResource thePatch) { 096 097 List<IBase> opParameters = ParametersUtil.getNamedParameters(myContext, thePatch, PARAMETER_OPERATION); 098 for (IBase nextOperation : opParameters) { 099 String type = ParametersUtil.getParameterPartValueAsString(myContext, nextOperation, PARAMETER_TYPE); 100 type = defaultString(type); 101 102 if (OPERATION_DELETE.equals(type)) { 103 handleDeleteOperation(theResource, nextOperation); 104 } else if (OPERATION_ADD.equals(type)) { 105 handleAddOperation(theResource, nextOperation); 106 } else if (OPERATION_REPLACE.equals(type)) { 107 handleReplaceOperation(theResource, nextOperation); 108 } else if (OPERATION_INSERT.equals(type)) { 109 handleInsertOperation(theResource, nextOperation); 110 } else if (OPERATION_MOVE.equals(type)) { 111 handleMoveOperation(theResource, nextOperation); 112 } else { 113 throw new InvalidRequestException(Msg.code(1267) + "Unknown patch operation type: " + type); 114 } 115 } 116 } 117 118 private void handleAddOperation(IBaseResource theResource, IBase theParameters) { 119 120 String path = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_PATH); 121 String elementName = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_NAME); 122 123 String containingPath = defaultString(path); 124 125 List<IBase> containingElements = myContext.newFhirPath().evaluate(theResource, containingPath, IBase.class); 126 for (IBase nextElement : containingElements) { 127 ChildDefinition childDefinition = findChildDefinition(nextElement, elementName); 128 129 IBase newValue = getNewValue(theParameters, nextElement, childDefinition); 130 131 childDefinition.getChildDef().getMutator().addValue(nextElement, newValue); 132 } 133 } 134 135 private void handleInsertOperation(IBaseResource theResource, IBase theParameters) { 136 137 String path = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_PATH); 138 path = defaultString(path); 139 140 int lastDot = path.lastIndexOf("."); 141 String containingPath = path.substring(0, lastDot); 142 String elementName = path.substring(lastDot + 1); 143 Integer insertIndex = ParametersUtil.getParameterPartValueAsInteger(myContext, theParameters, PARAMETER_INDEX) 144 .orElseThrow(() -> new InvalidRequestException("No index supplied for insert operation")); 145 146 List<IBase> containingElements = myContext.newFhirPath().evaluate(theResource, containingPath, IBase.class); 147 for (IBase nextElement : containingElements) { 148 149 ChildDefinition childDefinition = findChildDefinition(nextElement, elementName); 150 151 IBase newValue = getNewValue(theParameters, nextElement, childDefinition); 152 153 List<IBase> existingValues = 154 new ArrayList<>(childDefinition.getChildDef().getAccessor().getValues(nextElement)); 155 if (insertIndex == null || insertIndex < 0 || insertIndex > existingValues.size()) { 156 String msg = myContext 157 .getLocalizer() 158 .getMessage(FhirPatch.class, "invalidInsertIndex", insertIndex, path, existingValues.size()); 159 throw new InvalidRequestException(Msg.code(1270) + msg); 160 } 161 existingValues.add(insertIndex, newValue); 162 163 childDefinition.getChildDef().getMutator().setValue(nextElement, null); 164 for (IBase nextNewValue : existingValues) { 165 childDefinition.getChildDef().getMutator().addValue(nextElement, nextNewValue); 166 } 167 } 168 } 169 170 private void handleDeleteOperation(IBaseResource theResource, IBase theParameters) { 171 172 String path = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_PATH); 173 path = defaultString(path); 174 175 String containingPath; 176 String elementName; 177 178 if (path.endsWith(")")) { 179 // This is probably a filter, so we're probably dealing with a list 180 int filterArgsIndex = path.lastIndexOf('('); // Let's hope there aren't nested parentheses 181 int lastDotIndex = path.lastIndexOf( 182 '.', filterArgsIndex); // There might be a dot inside the parentheses, so look to the left of that 183 int secondLastDotIndex = path.lastIndexOf('.', lastDotIndex - 1); 184 containingPath = path.substring(0, secondLastDotIndex); 185 elementName = path.substring(secondLastDotIndex + 1, lastDotIndex); 186 } else if (path.endsWith("]")) { 187 // This is almost definitely a list 188 int openBracketIndex = path.lastIndexOf('['); 189 int lastDotIndex = path.lastIndexOf('.', openBracketIndex); 190 containingPath = path.substring(0, lastDotIndex); 191 elementName = path.substring(lastDotIndex + 1, openBracketIndex); 192 } else { 193 containingPath = path; 194 elementName = null; 195 } 196 197 List<IBase> containingElements = myContext.newFhirPath().evaluate(theResource, containingPath, IBase.class); 198 for (IBase nextElement : containingElements) { 199 if (elementName == null) { 200 deleteSingleElement(nextElement); 201 } else { 202 deleteFromList(theResource, nextElement, elementName, path); 203 } 204 } 205 } 206 207 private void deleteFromList( 208 IBaseResource theResource, 209 IBase theContainingElement, 210 String theListElementName, 211 String theElementToDeletePath) { 212 ChildDefinition childDefinition = findChildDefinition(theContainingElement, theListElementName); 213 214 List<IBase> existingValues = 215 new ArrayList<>(childDefinition.getChildDef().getAccessor().getValues(theContainingElement)); 216 List<IBase> elementsToRemove = 217 myContext.newFhirPath().evaluate(theResource, theElementToDeletePath, IBase.class); 218 existingValues.removeAll(elementsToRemove); 219 220 childDefinition.getChildDef().getMutator().setValue(theContainingElement, null); 221 for (IBase nextNewValue : existingValues) { 222 childDefinition.getChildDef().getMutator().addValue(theContainingElement, nextNewValue); 223 } 224 } 225 226 private void handleReplaceOperation(IBaseResource theResource, IBase theParameters) { 227 String path = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_PATH); 228 path = defaultString(path); 229 230 int lastDot = path.lastIndexOf("."); 231 String containingPath = path.substring(0, lastDot); 232 String elementName = path.substring(lastDot + 1); 233 234 List<IBase> containingElements = myContext.newFhirPath().evaluate(theResource, containingPath, IBase.class); 235 for (IBase nextElement : containingElements) { 236 237 ChildDefinition childDefinition = findChildDefinition(nextElement, elementName); 238 239 IBase newValue = getNewValue(theParameters, nextElement, childDefinition); 240 241 childDefinition.getChildDef().getMutator().setValue(nextElement, newValue); 242 } 243 } 244 245 private void handleMoveOperation(IBaseResource theResource, IBase theParameters) { 246 String path = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_PATH); 247 path = defaultString(path); 248 249 int lastDot = path.lastIndexOf("."); 250 String containingPath = path.substring(0, lastDot); 251 String elementName = path.substring(lastDot + 1); 252 Integer insertIndex = ParametersUtil.getParameterPartValueAsInteger( 253 myContext, theParameters, PARAMETER_DESTINATION) 254 .orElseThrow(() -> new InvalidRequestException("No index supplied for move operation")); 255 Integer removeIndex = ParametersUtil.getParameterPartValueAsInteger(myContext, theParameters, PARAMETER_SOURCE) 256 .orElseThrow(() -> new InvalidRequestException("No index supplied for move operation")); 257 258 List<IBase> containingElements = myContext.newFhirPath().evaluate(theResource, containingPath, IBase.class); 259 for (IBase nextElement : containingElements) { 260 261 ChildDefinition childDefinition = findChildDefinition(nextElement, elementName); 262 263 List<IBase> existingValues = 264 new ArrayList<>(childDefinition.getChildDef().getAccessor().getValues(nextElement)); 265 if (removeIndex == null || removeIndex < 0 || removeIndex >= existingValues.size()) { 266 String msg = myContext 267 .getLocalizer() 268 .getMessage( 269 FhirPatch.class, "invalidMoveSourceIndex", removeIndex, path, existingValues.size()); 270 throw new InvalidRequestException(Msg.code(1268) + msg); 271 } 272 IBase newValue = existingValues.remove(removeIndex.intValue()); 273 274 if (insertIndex == null || insertIndex < 0 || insertIndex > existingValues.size()) { 275 String msg = myContext 276 .getLocalizer() 277 .getMessage( 278 FhirPatch.class, 279 "invalidMoveDestinationIndex", 280 insertIndex, 281 path, 282 existingValues.size()); 283 throw new InvalidRequestException(Msg.code(1269) + msg); 284 } 285 existingValues.add(insertIndex, newValue); 286 287 childDefinition.getChildDef().getMutator().setValue(nextElement, null); 288 for (IBase nextNewValue : existingValues) { 289 childDefinition.getChildDef().getMutator().addValue(nextElement, nextNewValue); 290 } 291 } 292 } 293 294 private ChildDefinition findChildDefinition(IBase theContainingElement, String theElementName) { 295 BaseRuntimeElementDefinition<?> elementDef = myContext.getElementDefinition(theContainingElement.getClass()); 296 297 String childName = theElementName; 298 BaseRuntimeChildDefinition childDef = elementDef.getChildByName(childName); 299 BaseRuntimeElementDefinition<?> childElement; 300 if (childDef == null) { 301 childName = theElementName + "[x]"; 302 childDef = elementDef.getChildByName(childName); 303 childElement = childDef.getChildByName( 304 childDef.getValidChildNames().iterator().next()); 305 } else { 306 childElement = childDef.getChildByName(childName); 307 } 308 309 return new ChildDefinition(childDef, childElement); 310 } 311 312 private IBase getNewValue(IBase theParameters, IBase theElement, ChildDefinition theChildDefinition) { 313 Optional<IBase> valuePart = ParametersUtil.getParameterPart(myContext, theParameters, PARAMETER_VALUE); 314 Optional<IBase> valuePartValue = 315 ParametersUtil.getParameterPartValue(myContext, theParameters, PARAMETER_VALUE); 316 317 IBase newValue; 318 if (valuePartValue.isPresent()) { 319 newValue = valuePartValue.get(); 320 } else { 321 List<IBase> partParts = valuePart.map(this::extractPartsFromPart).orElse(Collections.emptyList()); 322 323 newValue = createAndPopulateNewElement(theChildDefinition, partParts); 324 } 325 326 if (IBaseEnumeration.class.isAssignableFrom( 327 theChildDefinition.getChildElement().getImplementingClass()) 328 || XhtmlNode.class.isAssignableFrom( 329 theChildDefinition.getChildElement().getImplementingClass())) { 330 // If the compositeElementDef is an IBaseEnumeration, we will use the actual compositeElementDef definition 331 // to build one, since 332 // it needs the right factory object passed to its constructor 333 IPrimitiveType<?> newValueInstance; 334 if (theChildDefinition.getChildDef().getInstanceConstructorArguments() != null) { 335 newValueInstance = (IPrimitiveType<?>) theChildDefinition 336 .getChildElement() 337 .newInstance(theChildDefinition.getChildDef().getInstanceConstructorArguments()); 338 } else { 339 newValueInstance = 340 (IPrimitiveType<?>) theChildDefinition.getChildElement().newInstance(); 341 } 342 newValueInstance.setValueAsString(((IPrimitiveType<?>) newValue).getValueAsString()); 343 theChildDefinition.getChildDef().getMutator().setValue(theElement, newValueInstance); 344 newValue = newValueInstance; 345 } 346 return newValue; 347 } 348 349 @Nonnull 350 private List<IBase> extractPartsFromPart(IBase theParametersParameterComponent) { 351 return myContext.newTerser().getValues(theParametersParameterComponent, "part"); 352 } 353 354 /** 355 * this method will instantiate an element according to the provided Definition and it according to 356 * the properties found in thePartParts. a part usually represent a datatype as a name/value[X] pair. 357 * it may also represent a complex type like an Extension. 358 * 359 * @param theDefinition wrapper around the runtime definition of the element to be populated 360 * @param thePartParts list of Part to populate the element that will be created from theDefinition 361 * @return an element that was created from theDefinition and populated with the parts 362 */ 363 private IBase createAndPopulateNewElement(ChildDefinition theDefinition, List<IBase> thePartParts) { 364 IBase newElement = theDefinition.getChildElement().newInstance(); 365 366 for (IBase nextValuePartPart : thePartParts) { 367 368 String name = myContext 369 .newTerser() 370 .getSingleValue(nextValuePartPart, PARAMETER_NAME, IPrimitiveType.class) 371 .map(IPrimitiveType::getValueAsString) 372 .orElse(null); 373 374 if (StringUtils.isBlank(name)) { 375 continue; 376 } 377 378 Optional<IBase> value = myContext.newTerser().getSingleValue(nextValuePartPart, "value[x]", IBase.class); 379 380 if (value.isPresent()) { 381 // we have a dataType. let's extract its value and assign it. 382 BaseRuntimeChildDefinition partChildDef = 383 theDefinition.getChildElement().getChildByName(name); 384 if (partChildDef == null) { 385 name = name + "[x]"; 386 partChildDef = theDefinition.getChildElement().getChildByName(name); 387 } 388 partChildDef.getMutator().addValue(newElement, value.get()); 389 390 // a part represent a datatype or a complexType but not both at the same time. 391 continue; 392 } 393 394 List<IBase> part = extractPartsFromPart(nextValuePartPart); 395 396 if (!part.isEmpty()) { 397 // we have a complexType. let's find its definition and recursively process 398 // them till all complexTypes are processed. 399 ChildDefinition childDefinition = findChildDefinition(newElement, name); 400 401 IBase childNewValue = createAndPopulateNewElement(childDefinition, part); 402 403 childDefinition.getChildDef().getMutator().setValue(newElement, childNewValue); 404 } 405 } 406 407 return newElement; 408 } 409 410 private void deleteSingleElement(IBase theElementToDelete) { 411 myContext.newTerser().visit(theElementToDelete, new IModelVisitor2() { 412 @Override 413 public boolean acceptElement( 414 IBase theElement, 415 List<IBase> theContainingElementPath, 416 List<BaseRuntimeChildDefinition> theChildDefinitionPath, 417 List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) { 418 if (theElement instanceof IPrimitiveType) { 419 ((IPrimitiveType<?>) theElement).setValueAsString(null); 420 } 421 return true; 422 } 423 }); 424 } 425 426 public IBaseParameters diff(@Nullable IBaseResource theOldValue, @Nonnull IBaseResource theNewValue) { 427 IBaseParameters retVal = ParametersUtil.newInstance(myContext); 428 String newValueTypeName = myContext.getResourceDefinition(theNewValue).getName(); 429 430 if (theOldValue == null) { 431 432 IBase operation = ParametersUtil.addParameterToParameters(myContext, retVal, PARAMETER_OPERATION); 433 ParametersUtil.addPartCode(myContext, operation, PARAMETER_TYPE, OPERATION_INSERT); 434 ParametersUtil.addPartString(myContext, operation, PARAMETER_PATH, newValueTypeName); 435 ParametersUtil.addPart(myContext, operation, PARAMETER_VALUE, theNewValue); 436 437 } else { 438 439 String oldValueTypeName = 440 myContext.getResourceDefinition(theOldValue).getName(); 441 Validate.isTrue(oldValueTypeName.equalsIgnoreCase(newValueTypeName), "Resources must be of same type"); 442 443 BaseRuntimeElementCompositeDefinition<?> def = 444 myContext.getResourceDefinition(theOldValue).getBaseDefinition(); 445 String path = def.getName(); 446 447 EncodeContextPath contextPath = new EncodeContextPath(); 448 contextPath.pushPath(path, true); 449 450 compare(retVal, contextPath, def, path, path, theOldValue, theNewValue); 451 452 contextPath.popPath(); 453 assert contextPath.getPath().isEmpty(); 454 } 455 456 return retVal; 457 } 458 459 private void compare( 460 IBaseParameters theDiff, 461 EncodeContextPath theSourceEncodeContext, 462 BaseRuntimeElementDefinition<?> theDef, 463 String theSourcePath, 464 String theTargetPath, 465 IBase theOldField, 466 IBase theNewField) { 467 468 boolean pathIsIgnored = pathIsIgnored(theSourceEncodeContext); 469 if (pathIsIgnored) { 470 return; 471 } 472 473 BaseRuntimeElementDefinition<?> sourceDef = myContext.getElementDefinition(theOldField.getClass()); 474 BaseRuntimeElementDefinition<?> targetDef = myContext.getElementDefinition(theNewField.getClass()); 475 if (!sourceDef.getName().equals(targetDef.getName())) { 476 IBase operation = ParametersUtil.addParameterToParameters(myContext, theDiff, PARAMETER_OPERATION); 477 ParametersUtil.addPartCode(myContext, operation, PARAMETER_TYPE, OPERATION_REPLACE); 478 ParametersUtil.addPartString(myContext, operation, PARAMETER_PATH, theTargetPath); 479 addValueToDiff(operation, theOldField, theNewField); 480 } else { 481 if (theOldField instanceof IPrimitiveType) { 482 IPrimitiveType<?> oldPrimitive = (IPrimitiveType<?>) theOldField; 483 IPrimitiveType<?> newPrimitive = (IPrimitiveType<?>) theNewField; 484 String oldValueAsString = toValue(oldPrimitive); 485 String newValueAsString = toValue(newPrimitive); 486 if (!Objects.equals(oldValueAsString, newValueAsString)) { 487 IBase operation = ParametersUtil.addParameterToParameters(myContext, theDiff, PARAMETER_OPERATION); 488 ParametersUtil.addPartCode(myContext, operation, PARAMETER_TYPE, OPERATION_REPLACE); 489 ParametersUtil.addPartString(myContext, operation, PARAMETER_PATH, theTargetPath); 490 addValueToDiff(operation, oldPrimitive, newPrimitive); 491 } 492 } 493 494 List<BaseRuntimeChildDefinition> children = theDef.getChildren(); 495 for (BaseRuntimeChildDefinition nextChild : children) { 496 compareField( 497 theDiff, 498 theSourceEncodeContext, 499 theSourcePath, 500 theTargetPath, 501 theOldField, 502 theNewField, 503 nextChild); 504 } 505 } 506 } 507 508 private void compareField( 509 IBaseParameters theDiff, 510 EncodeContextPath theSourceEncodePath, 511 String theSourcePath, 512 String theTargetPath, 513 IBase theOldField, 514 IBase theNewField, 515 BaseRuntimeChildDefinition theChildDef) { 516 String elementName = theChildDef.getElementName(); 517 boolean repeatable = theChildDef.getMax() != 1; 518 theSourceEncodePath.pushPath(elementName, false); 519 if (pathIsIgnored(theSourceEncodePath)) { 520 theSourceEncodePath.popPath(); 521 return; 522 } 523 524 List<? extends IBase> sourceValues = theChildDef.getAccessor().getValues(theOldField); 525 List<? extends IBase> targetValues = theChildDef.getAccessor().getValues(theNewField); 526 527 int sourceIndex = 0; 528 int targetIndex = 0; 529 while (sourceIndex < sourceValues.size() && targetIndex < targetValues.size()) { 530 531 IBase sourceChildField = sourceValues.get(sourceIndex); 532 Validate.notNull(sourceChildField); // not expected to happen, but just in case 533 BaseRuntimeElementDefinition<?> def = myContext.getElementDefinition(sourceChildField.getClass()); 534 IBase targetChildField = targetValues.get(targetIndex); 535 Validate.notNull(targetChildField); // not expected to happen, but just in case 536 String sourcePath = theSourcePath + "." + elementName + (repeatable ? "[" + sourceIndex + "]" : ""); 537 String targetPath = theSourcePath + "." + elementName + (repeatable ? "[" + targetIndex + "]" : ""); 538 539 compare(theDiff, theSourceEncodePath, def, sourcePath, targetPath, sourceChildField, targetChildField); 540 541 sourceIndex++; 542 targetIndex++; 543 } 544 545 // Find newly inserted items 546 while (targetIndex < targetValues.size()) { 547 String path = theTargetPath + "." + elementName; 548 addInsertItems(theDiff, targetValues, targetIndex, path, theChildDef); 549 targetIndex++; 550 } 551 552 // Find deleted items 553 while (sourceIndex < sourceValues.size()) { 554 IBase operation = ParametersUtil.addParameterToParameters(myContext, theDiff, PARAMETER_OPERATION); 555 ParametersUtil.addPartCode(myContext, operation, PARAMETER_TYPE, OPERATION_DELETE); 556 ParametersUtil.addPartString( 557 myContext, 558 operation, 559 PARAMETER_PATH, 560 theTargetPath + "." + elementName + (repeatable ? "[" + targetIndex + "]" : "")); 561 562 sourceIndex++; 563 targetIndex++; 564 } 565 566 theSourceEncodePath.popPath(); 567 } 568 569 private void addInsertItems( 570 IBaseParameters theDiff, 571 List<? extends IBase> theTargetValues, 572 int theTargetIndex, 573 String thePath, 574 BaseRuntimeChildDefinition theChildDefinition) { 575 IBase operation = ParametersUtil.addParameterToParameters(myContext, theDiff, PARAMETER_OPERATION); 576 ParametersUtil.addPartCode(myContext, operation, PARAMETER_TYPE, OPERATION_INSERT); 577 ParametersUtil.addPartString(myContext, operation, PARAMETER_PATH, thePath); 578 ParametersUtil.addPartInteger(myContext, operation, PARAMETER_INDEX, theTargetIndex); 579 580 IBase value = theTargetValues.get(theTargetIndex); 581 BaseRuntimeElementDefinition<?> valueDef = myContext.getElementDefinition(value.getClass()); 582 583 /* 584 * If the value is a Resource or a datatype, we can put it into the part.value and that will cover 585 * all of its children. If it's an infrastructure element though, such as Patient.contact we can't 586 * just put it into part.value because it isn't an actual type. So we have to put all of its 587 * children in instead. 588 */ 589 if (valueDef.isStandardType()) { 590 ParametersUtil.addPart(myContext, operation, PARAMETER_VALUE, value); 591 } else { 592 for (BaseRuntimeChildDefinition nextChild : valueDef.getChildren()) { 593 List<IBase> childValues = nextChild.getAccessor().getValues(value); 594 for (int index = 0; index < childValues.size(); index++) { 595 boolean childRepeatable = theChildDefinition.getMax() != 1; 596 String elementName = nextChild.getChildNameByDatatype( 597 childValues.get(index).getClass()); 598 String targetPath = thePath + (childRepeatable ? "[" + index + "]" : "") + "." + elementName; 599 addInsertItems(theDiff, childValues, index, targetPath, nextChild); 600 } 601 } 602 } 603 } 604 605 private void addValueToDiff(IBase theOperationPart, IBase theOldValue, IBase theNewValue) { 606 607 if (myIncludePreviousValueInDiff) { 608 IBase oldValue = massageValueForDiff(theOldValue); 609 ParametersUtil.addPart(myContext, theOperationPart, "previousValue", oldValue); 610 } 611 612 IBase newValue = massageValueForDiff(theNewValue); 613 ParametersUtil.addPart(myContext, theOperationPart, PARAMETER_VALUE, newValue); 614 } 615 616 private boolean pathIsIgnored(EncodeContextPath theSourceEncodeContext) { 617 boolean pathIsIgnored = false; 618 for (EncodeContextPath next : myIgnorePaths) { 619 if (theSourceEncodeContext.startsWith(next, false)) { 620 pathIsIgnored = true; 621 break; 622 } 623 } 624 return pathIsIgnored; 625 } 626 627 private IBase massageValueForDiff(IBase theNewValue) { 628 IBase massagedValue = theNewValue; 629 630 // XHTML content is dealt with by putting it in a string 631 if (theNewValue instanceof XhtmlNode) { 632 String xhtmlString = ((XhtmlNode) theNewValue).getValueAsString(); 633 massagedValue = myContext.getElementDefinition("string").newInstance(xhtmlString); 634 } 635 636 // IIdType can hold a fully qualified ID, but we just want the ID part to show up in diffs 637 if (theNewValue instanceof IIdType) { 638 String idPart = ((IIdType) theNewValue).getIdPart(); 639 massagedValue = myContext.getElementDefinition("id").newInstance(idPart); 640 } 641 642 return massagedValue; 643 } 644 645 private String toValue(IPrimitiveType<?> theOldPrimitive) { 646 if (theOldPrimitive instanceof IIdType) { 647 return ((IIdType) theOldPrimitive).getIdPart(); 648 } 649 return theOldPrimitive.getValueAsString(); 650 } 651 652 private static class ChildDefinition { 653 private final BaseRuntimeChildDefinition myChildDef; 654 private final BaseRuntimeElementDefinition<?> myChildElement; 655 656 public ChildDefinition( 657 BaseRuntimeChildDefinition theChildDef, BaseRuntimeElementDefinition<?> theChildElement) { 658 this.myChildDef = theChildDef; 659 this.myChildElement = theChildElement; 660 } 661 662 public BaseRuntimeChildDefinition getChildDef() { 663 return myChildDef; 664 } 665 666 public BaseRuntimeElementDefinition<?> getChildElement() { 667 return myChildElement; 668 } 669 } 670}