
001/*- 002 * #%L 003 * HAPI FHIR Storage api 004 * %% 005 * Copyright (C) 2014 - 2025 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 java.util.Objects.isNull; 052import static org.apache.commons.lang3.StringUtils.defaultString; 053 054public class FhirPatch { 055 056 public static final String OPERATION_ADD = "add"; 057 public static final String OPERATION_DELETE = "delete"; 058 public static final String OPERATION_INSERT = "insert"; 059 public static final String OPERATION_MOVE = "move"; 060 public static final String OPERATION_REPLACE = "replace"; 061 public static final String PARAMETER_DESTINATION = "destination"; 062 public static final String PARAMETER_INDEX = "index"; 063 public static final String PARAMETER_NAME = "name"; 064 public static final String PARAMETER_OPERATION = "operation"; 065 public static final String PARAMETER_PATH = "path"; 066 public static final String PARAMETER_SOURCE = "source"; 067 public static final String PARAMETER_TYPE = "type"; 068 public static final String PARAMETER_VALUE = "value"; 069 070 private final FhirContext myContext; 071 private boolean myIncludePreviousValueInDiff; 072 private Set<EncodeContextPath> myIgnorePaths = Collections.emptySet(); 073 074 public FhirPatch(FhirContext theContext) { 075 myContext = theContext; 076 } 077 078 /** 079 * Adds a path element that will not be included in generated diffs. Values can take the form 080 * <code>ResourceName.fieldName.fieldName</code> and wildcards are supported, such 081 * as <code>*.meta</code>. 082 */ 083 public void addIgnorePath(String theIgnorePath) { 084 Validate.notBlank(theIgnorePath, "theIgnorePath must not be null or empty"); 085 086 if (myIgnorePaths.isEmpty()) { 087 myIgnorePaths = new HashSet<>(); 088 } 089 myIgnorePaths.add(new EncodeContextPath(theIgnorePath)); 090 } 091 092 public void setIncludePreviousValueInDiff(boolean theIncludePreviousValueInDiff) { 093 myIncludePreviousValueInDiff = theIncludePreviousValueInDiff; 094 } 095 096 public void apply(IBaseResource theResource, IBaseResource thePatch) { 097 098 List<IBase> opParameters = ParametersUtil.getNamedParameters(myContext, thePatch, PARAMETER_OPERATION); 099 for (IBase nextOperation : opParameters) { 100 String type = ParametersUtil.getParameterPartValueAsString(myContext, nextOperation, PARAMETER_TYPE); 101 type = defaultString(type); 102 103 if (OPERATION_DELETE.equals(type)) { 104 handleDeleteOperation(theResource, nextOperation); 105 } else if (OPERATION_ADD.equals(type)) { 106 handleAddOperation(theResource, nextOperation); 107 } else if (OPERATION_REPLACE.equals(type)) { 108 handleReplaceOperation(theResource, nextOperation); 109 } else if (OPERATION_INSERT.equals(type)) { 110 handleInsertOperation(theResource, nextOperation); 111 } else if (OPERATION_MOVE.equals(type)) { 112 handleMoveOperation(theResource, nextOperation); 113 } else { 114 throw new InvalidRequestException(Msg.code(1267) + "Unknown patch operation type: " + type); 115 } 116 } 117 } 118 119 private void handleAddOperation(IBaseResource theResource, IBase theParameters) { 120 121 String path = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_PATH); 122 String elementName = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_NAME); 123 124 String containingPath = defaultString(path); 125 126 List<IBase> containingElements = myContext.newFhirPath().evaluate(theResource, containingPath, IBase.class); 127 for (IBase nextElement : containingElements) { 128 ChildDefinition childDefinition = findChildDefinition(nextElement, elementName); 129 130 IBase newValue = getNewValue(theParameters, childDefinition); 131 132 childDefinition.getChildDef().getMutator().addValue(nextElement, newValue); 133 } 134 } 135 136 private void handleInsertOperation(IBaseResource theResource, IBase theParameters) { 137 138 String path = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_PATH); 139 path = defaultString(path); 140 141 int lastDot = path.lastIndexOf("."); 142 String containingPath = path.substring(0, lastDot); 143 String elementName = path.substring(lastDot + 1); 144 Integer insertIndex = ParametersUtil.getParameterPartValueAsInteger(myContext, theParameters, PARAMETER_INDEX) 145 .orElseThrow(() -> new InvalidRequestException("No index supplied for insert operation")); 146 147 List<IBase> containingElements = myContext.newFhirPath().evaluate(theResource, containingPath, IBase.class); 148 for (IBase nextElement : containingElements) { 149 150 ChildDefinition childDefinition = findChildDefinition(nextElement, elementName); 151 152 IBase newValue = getNewValue(theParameters, childDefinition); 153 154 List<IBase> existingValues = 155 new ArrayList<>(childDefinition.getChildDef().getAccessor().getValues(nextElement)); 156 if (insertIndex == null || insertIndex < 0 || insertIndex > existingValues.size()) { 157 String msg = myContext 158 .getLocalizer() 159 .getMessage(FhirPatch.class, "invalidInsertIndex", insertIndex, path, existingValues.size()); 160 throw new InvalidRequestException(Msg.code(1270) + msg); 161 } 162 existingValues.add(insertIndex, newValue); 163 164 childDefinition.getChildDef().getMutator().setValue(nextElement, null); 165 for (IBase nextNewValue : existingValues) { 166 childDefinition.getChildDef().getMutator().addValue(nextElement, nextNewValue); 167 } 168 } 169 } 170 171 private void handleDeleteOperation(IBaseResource theResource, IBase theParameters) { 172 173 String path = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_PATH); 174 path = defaultString(path); 175 176 ParsedPath parsedPath = ParsedPath.parse(path); 177 List<IBase> containingElements = myContext 178 .newFhirPath() 179 .evaluate( 180 theResource, 181 parsedPath.getEndsWithAFilterOrIndex() ? parsedPath.getContainingPath() : path, 182 IBase.class); 183 184 for (IBase nextElement : containingElements) { 185 if (parsedPath.getEndsWithAFilterOrIndex()) { 186 // if the path ends with a filter or index, we must be dealing with a list 187 deleteFromList(theResource, nextElement, parsedPath.getLastElementName(), path); 188 } else { 189 deleteSingleElement(nextElement); 190 } 191 } 192 } 193 194 private void deleteFromList( 195 IBaseResource theResource, 196 IBase theContainingElement, 197 String theListElementName, 198 String theElementToDeletePath) { 199 ChildDefinition childDefinition = findChildDefinition(theContainingElement, theListElementName); 200 201 List<IBase> existingValues = 202 new ArrayList<>(childDefinition.getChildDef().getAccessor().getValues(theContainingElement)); 203 List<IBase> elementsToRemove = 204 myContext.newFhirPath().evaluate(theResource, theElementToDeletePath, IBase.class); 205 existingValues.removeAll(elementsToRemove); 206 207 childDefinition.getChildDef().getMutator().setValue(theContainingElement, null); 208 for (IBase nextNewValue : existingValues) { 209 childDefinition.getChildDef().getMutator().addValue(theContainingElement, nextNewValue); 210 } 211 } 212 213 private void handleReplaceOperation(IBaseResource theResource, IBase theParameters) { 214 String path = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_PATH); 215 path = defaultString(path); 216 217 ParsedPath parsedPath = ParsedPath.parse(path); 218 219 List<IBase> containingElements = 220 myContext.newFhirPath().evaluate(theResource, parsedPath.getContainingPath(), IBase.class); 221 for (IBase containingElement : containingElements) { 222 223 ChildDefinition childDefinition = findChildDefinition(containingElement, parsedPath.getLastElementName()); 224 IBase newValue = getNewValue(theParameters, childDefinition); 225 if (parsedPath.getEndsWithAFilterOrIndex()) { 226 // if the path ends with a filter or index, we must be dealing with a list 227 replaceInList(newValue, theResource, containingElement, childDefinition, path); 228 } else { 229 childDefinition.getChildDef().getMutator().setValue(containingElement, newValue); 230 } 231 } 232 } 233 234 private void replaceInList( 235 IBase theNewValue, 236 IBaseResource theResource, 237 IBase theContainingElement, 238 ChildDefinition theChildDefinitionForTheList, 239 String theFullReplacePath) { 240 241 List<IBase> existingValues = new ArrayList<>( 242 theChildDefinitionForTheList.getChildDef().getAccessor().getValues(theContainingElement)); 243 List<IBase> valuesToReplace = myContext.newFhirPath().evaluate(theResource, theFullReplacePath, IBase.class); 244 if (valuesToReplace.isEmpty()) { 245 String msg = myContext 246 .getLocalizer() 247 .getMessage(FhirPatch.class, "noMatchingElementForPath", theFullReplacePath); 248 throw new InvalidRequestException(Msg.code(2617) + msg); 249 } 250 251 BaseRuntimeChildDefinition.IMutator listMutator = 252 theChildDefinitionForTheList.getChildDef().getMutator(); 253 // clear the whole list first, then reconstruct it in the loop below replacing the values that need to be 254 // replaced 255 listMutator.setValue(theContainingElement, null); 256 for (IBase existingValue : existingValues) { 257 if (valuesToReplace.contains(existingValue)) { 258 listMutator.addValue(theContainingElement, theNewValue); 259 } else { 260 listMutator.addValue(theContainingElement, existingValue); 261 } 262 } 263 } 264 265 private void handleMoveOperation(IBaseResource theResource, IBase theParameters) { 266 String path = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_PATH); 267 path = defaultString(path); 268 269 int lastDot = path.lastIndexOf("."); 270 String containingPath = path.substring(0, lastDot); 271 String elementName = path.substring(lastDot + 1); 272 Integer insertIndex = ParametersUtil.getParameterPartValueAsInteger( 273 myContext, theParameters, PARAMETER_DESTINATION) 274 .orElseThrow(() -> new InvalidRequestException("No index supplied for move operation")); 275 Integer removeIndex = ParametersUtil.getParameterPartValueAsInteger(myContext, theParameters, PARAMETER_SOURCE) 276 .orElseThrow(() -> new InvalidRequestException("No index supplied for move operation")); 277 278 List<IBase> containingElements = myContext.newFhirPath().evaluate(theResource, containingPath, IBase.class); 279 for (IBase nextElement : containingElements) { 280 281 ChildDefinition childDefinition = findChildDefinition(nextElement, elementName); 282 283 List<IBase> existingValues = 284 new ArrayList<>(childDefinition.getChildDef().getAccessor().getValues(nextElement)); 285 if (removeIndex == null || removeIndex < 0 || removeIndex >= existingValues.size()) { 286 String msg = myContext 287 .getLocalizer() 288 .getMessage( 289 FhirPatch.class, "invalidMoveSourceIndex", removeIndex, path, existingValues.size()); 290 throw new InvalidRequestException(Msg.code(1268) + msg); 291 } 292 IBase newValue = existingValues.remove(removeIndex.intValue()); 293 294 if (insertIndex == null || insertIndex < 0 || insertIndex > existingValues.size()) { 295 String msg = myContext 296 .getLocalizer() 297 .getMessage( 298 FhirPatch.class, 299 "invalidMoveDestinationIndex", 300 insertIndex, 301 path, 302 existingValues.size()); 303 throw new InvalidRequestException(Msg.code(1269) + msg); 304 } 305 existingValues.add(insertIndex, newValue); 306 307 childDefinition.getChildDef().getMutator().setValue(nextElement, null); 308 for (IBase nextNewValue : existingValues) { 309 childDefinition.getChildDef().getMutator().addValue(nextElement, nextNewValue); 310 } 311 } 312 } 313 314 private ChildDefinition findChildDefinition(IBase theContainingElement, String theElementName) { 315 BaseRuntimeElementDefinition<?> elementDef = myContext.getElementDefinition(theContainingElement.getClass()); 316 317 String childName = theElementName; 318 BaseRuntimeChildDefinition childDef = elementDef.getChildByName(childName); 319 BaseRuntimeElementDefinition<?> childElement; 320 if (childDef == null) { 321 childName = theElementName + "[x]"; 322 childDef = elementDef.getChildByName(childName); 323 childElement = childDef.getChildByName( 324 childDef.getValidChildNames().iterator().next()); 325 } else { 326 childElement = childDef.getChildByName(childName); 327 } 328 329 return new ChildDefinition(childDef, childElement); 330 } 331 332 private IBase getNewValue(IBase theParameters, ChildDefinition theChildDefinition) { 333 Optional<IBase> valuePart = ParametersUtil.getParameterPart(myContext, theParameters, PARAMETER_VALUE); 334 Optional<IBase> valuePartValue = 335 ParametersUtil.getParameterPartValue(myContext, theParameters, PARAMETER_VALUE); 336 337 IBase newValue; 338 if (valuePartValue.isPresent()) { 339 newValue = maybeMassageToEnumeration(valuePartValue.get(), theChildDefinition); 340 341 } else { 342 List<IBase> partParts = valuePart.map(this::extractPartsFromPart).orElse(Collections.emptyList()); 343 344 newValue = createAndPopulateNewElement(theChildDefinition, partParts); 345 } 346 347 return newValue; 348 } 349 350 private IBase maybeMassageToEnumeration(IBase theValue, ChildDefinition theChildDefinition) { 351 IBase retVal = theValue; 352 353 if (IBaseEnumeration.class.isAssignableFrom( 354 theChildDefinition.getChildElement().getImplementingClass()) 355 || XhtmlNode.class.isAssignableFrom( 356 theChildDefinition.getChildElement().getImplementingClass())) { 357 // If the compositeElementDef is an IBaseEnumeration, we will use the actual compositeElementDef definition 358 // to build one, since 359 // it needs the right factory object passed to its constructor 360 IPrimitiveType<?> newValueInstance; 361 if (theChildDefinition.getChildDef().getInstanceConstructorArguments() != null) { 362 newValueInstance = (IPrimitiveType<?>) theChildDefinition 363 .getChildElement() 364 .newInstance(theChildDefinition.getChildDef().getInstanceConstructorArguments()); 365 } else { 366 newValueInstance = 367 (IPrimitiveType<?>) theChildDefinition.getChildElement().newInstance(); 368 } 369 newValueInstance.setValueAsString(((IPrimitiveType<?>) theValue).getValueAsString()); 370 retVal = newValueInstance; 371 } 372 373 return retVal; 374 } 375 376 @Nonnull 377 private List<IBase> extractPartsFromPart(IBase theParametersParameterComponent) { 378 return myContext.newTerser().getValues(theParametersParameterComponent, "part"); 379 } 380 381 /** 382 * this method will instantiate an element according to the provided Definition and initialize its fields 383 * from the values provided in thePartParts. a part usually represent a datatype as a name/value[X] pair. 384 * it may also represent a complex type like an Extension. 385 * 386 * @param theDefinition wrapper around the runtime definition of the element to be populated 387 * @param thePartParts list of Part to populate the element that will be created from theDefinition 388 * @return an element that was created from theDefinition and populated with the parts 389 */ 390 private IBase createAndPopulateNewElement(ChildDefinition theDefinition, List<IBase> thePartParts) { 391 IBase newElement = theDefinition.getChildElement().newInstance(); 392 393 for (IBase nextValuePartPart : thePartParts) { 394 395 String name = myContext 396 .newTerser() 397 .getSingleValue(nextValuePartPart, PARAMETER_NAME, IPrimitiveType.class) 398 .map(IPrimitiveType::getValueAsString) 399 .orElse(null); 400 401 if (StringUtils.isBlank(name)) { 402 continue; 403 } 404 405 Optional<IBase> optionalValue = 406 myContext.newTerser().getSingleValue(nextValuePartPart, "value[x]", IBase.class); 407 408 if (optionalValue.isPresent()) { 409 // we have a dataType. let's extract its value and assign it. 410 411 ChildDefinition childDefinition = findChildDefinition(newElement, name); 412 IBase newValue = maybeMassageToEnumeration(optionalValue.get(), childDefinition); 413 414 BaseRuntimeChildDefinition partChildDef = 415 theDefinition.getChildElement().getChildByName(name); 416 417 if (isNull(partChildDef)) { 418 name = name + "[x]"; 419 partChildDef = theDefinition.getChildElement().getChildByName(name); 420 } 421 422 partChildDef.getMutator().setValue(newElement, newValue); 423 424 // a part represent a datatype or a complexType but not both at the same time. 425 continue; 426 } 427 428 List<IBase> part = extractPartsFromPart(nextValuePartPart); 429 430 if (!part.isEmpty()) { 431 // we have a complexType. let's find its definition and recursively process 432 // them till all complexTypes are processed. 433 ChildDefinition childDefinition = findChildDefinition(newElement, name); 434 435 IBase childNewValue = createAndPopulateNewElement(childDefinition, part); 436 437 childDefinition.getChildDef().getMutator().setValue(newElement, childNewValue); 438 } 439 } 440 441 return newElement; 442 } 443 444 private void deleteSingleElement(IBase theElementToDelete) { 445 myContext.newTerser().visit(theElementToDelete, new IModelVisitor2() { 446 @Override 447 public boolean acceptElement( 448 IBase theElement, 449 List<IBase> theContainingElementPath, 450 List<BaseRuntimeChildDefinition> theChildDefinitionPath, 451 List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) { 452 if (theElement instanceof IPrimitiveType) { 453 ((IPrimitiveType<?>) theElement).setValueAsString(null); 454 } 455 return true; 456 } 457 }); 458 } 459 460 public IBaseParameters diff(@Nullable IBaseResource theOldValue, @Nonnull IBaseResource theNewValue) { 461 IBaseParameters retVal = ParametersUtil.newInstance(myContext); 462 String newValueTypeName = myContext.getResourceDefinition(theNewValue).getName(); 463 464 if (theOldValue == null) { 465 466 IBase operation = ParametersUtil.addParameterToParameters(myContext, retVal, PARAMETER_OPERATION); 467 ParametersUtil.addPartCode(myContext, operation, PARAMETER_TYPE, OPERATION_INSERT); 468 ParametersUtil.addPartString(myContext, operation, PARAMETER_PATH, newValueTypeName); 469 ParametersUtil.addPart(myContext, operation, PARAMETER_VALUE, theNewValue); 470 471 } else { 472 473 String oldValueTypeName = 474 myContext.getResourceDefinition(theOldValue).getName(); 475 Validate.isTrue(oldValueTypeName.equalsIgnoreCase(newValueTypeName), "Resources must be of same type"); 476 477 BaseRuntimeElementCompositeDefinition<?> def = 478 myContext.getResourceDefinition(theOldValue).getBaseDefinition(); 479 String path = def.getName(); 480 481 EncodeContextPath contextPath = new EncodeContextPath(); 482 contextPath.pushPath(path, true); 483 484 compare(retVal, contextPath, def, path, path, theOldValue, theNewValue); 485 486 contextPath.popPath(); 487 assert contextPath.getPath().isEmpty(); 488 } 489 490 return retVal; 491 } 492 493 private void compare( 494 IBaseParameters theDiff, 495 EncodeContextPath theSourceEncodeContext, 496 BaseRuntimeElementDefinition<?> theDef, 497 String theSourcePath, 498 String theTargetPath, 499 IBase theOldField, 500 IBase theNewField) { 501 502 boolean pathIsIgnored = pathIsIgnored(theSourceEncodeContext); 503 if (pathIsIgnored) { 504 return; 505 } 506 507 BaseRuntimeElementDefinition<?> sourceDef = myContext.getElementDefinition(theOldField.getClass()); 508 BaseRuntimeElementDefinition<?> targetDef = myContext.getElementDefinition(theNewField.getClass()); 509 if (!sourceDef.getName().equals(targetDef.getName())) { 510 IBase operation = ParametersUtil.addParameterToParameters(myContext, theDiff, PARAMETER_OPERATION); 511 ParametersUtil.addPartCode(myContext, operation, PARAMETER_TYPE, OPERATION_REPLACE); 512 ParametersUtil.addPartString(myContext, operation, PARAMETER_PATH, theTargetPath); 513 addValueToDiff(operation, theOldField, theNewField); 514 } else { 515 if (theOldField instanceof IPrimitiveType) { 516 IPrimitiveType<?> oldPrimitive = (IPrimitiveType<?>) theOldField; 517 IPrimitiveType<?> newPrimitive = (IPrimitiveType<?>) theNewField; 518 String oldValueAsString = toValue(oldPrimitive); 519 String newValueAsString = toValue(newPrimitive); 520 if (!Objects.equals(oldValueAsString, newValueAsString)) { 521 IBase operation = ParametersUtil.addParameterToParameters(myContext, theDiff, PARAMETER_OPERATION); 522 ParametersUtil.addPartCode(myContext, operation, PARAMETER_TYPE, OPERATION_REPLACE); 523 ParametersUtil.addPartString(myContext, operation, PARAMETER_PATH, theTargetPath); 524 addValueToDiff(operation, oldPrimitive, newPrimitive); 525 } 526 } 527 528 List<BaseRuntimeChildDefinition> children = theDef.getChildren(); 529 for (BaseRuntimeChildDefinition nextChild : children) { 530 compareField( 531 theDiff, 532 theSourceEncodeContext, 533 theSourcePath, 534 theTargetPath, 535 theOldField, 536 theNewField, 537 nextChild); 538 } 539 } 540 } 541 542 private void compareField( 543 IBaseParameters theDiff, 544 EncodeContextPath theSourceEncodePath, 545 String theSourcePath, 546 String theTargetPath, 547 IBase theOldField, 548 IBase theNewField, 549 BaseRuntimeChildDefinition theChildDef) { 550 String elementName = theChildDef.getElementName(); 551 boolean repeatable = theChildDef.getMax() != 1; 552 theSourceEncodePath.pushPath(elementName, false); 553 if (pathIsIgnored(theSourceEncodePath)) { 554 theSourceEncodePath.popPath(); 555 return; 556 } 557 558 List<? extends IBase> sourceValues = theChildDef.getAccessor().getValues(theOldField); 559 List<? extends IBase> targetValues = theChildDef.getAccessor().getValues(theNewField); 560 561 int sourceIndex = 0; 562 int targetIndex = 0; 563 while (sourceIndex < sourceValues.size() && targetIndex < targetValues.size()) { 564 565 IBase sourceChildField = sourceValues.get(sourceIndex); 566 Validate.notNull(sourceChildField); // not expected to happen, but just in case 567 BaseRuntimeElementDefinition<?> def = myContext.getElementDefinition(sourceChildField.getClass()); 568 IBase targetChildField = targetValues.get(targetIndex); 569 Validate.notNull(targetChildField); // not expected to happen, but just in case 570 String sourcePath = theSourcePath + "." + elementName + (repeatable ? "[" + sourceIndex + "]" : ""); 571 String targetPath = theSourcePath + "." + elementName + (repeatable ? "[" + targetIndex + "]" : ""); 572 573 compare(theDiff, theSourceEncodePath, def, sourcePath, targetPath, sourceChildField, targetChildField); 574 575 sourceIndex++; 576 targetIndex++; 577 } 578 579 // Find newly inserted items 580 while (targetIndex < targetValues.size()) { 581 String path = theTargetPath + "." + elementName; 582 addInsertItems(theDiff, targetValues, targetIndex, path, theChildDef); 583 targetIndex++; 584 } 585 586 // Find deleted items 587 while (sourceIndex < sourceValues.size()) { 588 IBase operation = ParametersUtil.addParameterToParameters(myContext, theDiff, PARAMETER_OPERATION); 589 ParametersUtil.addPartCode(myContext, operation, PARAMETER_TYPE, OPERATION_DELETE); 590 ParametersUtil.addPartString( 591 myContext, 592 operation, 593 PARAMETER_PATH, 594 theTargetPath + "." + elementName + (repeatable ? "[" + targetIndex + "]" : "")); 595 596 sourceIndex++; 597 targetIndex++; 598 } 599 600 theSourceEncodePath.popPath(); 601 } 602 603 private void addInsertItems( 604 IBaseParameters theDiff, 605 List<? extends IBase> theTargetValues, 606 int theTargetIndex, 607 String thePath, 608 BaseRuntimeChildDefinition theChildDefinition) { 609 IBase operation = ParametersUtil.addParameterToParameters(myContext, theDiff, PARAMETER_OPERATION); 610 ParametersUtil.addPartCode(myContext, operation, PARAMETER_TYPE, OPERATION_INSERT); 611 ParametersUtil.addPartString(myContext, operation, PARAMETER_PATH, thePath); 612 ParametersUtil.addPartInteger(myContext, operation, PARAMETER_INDEX, theTargetIndex); 613 614 IBase value = theTargetValues.get(theTargetIndex); 615 BaseRuntimeElementDefinition<?> valueDef = myContext.getElementDefinition(value.getClass()); 616 617 /* 618 * If the value is a Resource or a datatype, we can put it into the part.value and that will cover 619 * all of its children. If it's an infrastructure element though, such as Patient.contact we can't 620 * just put it into part.value because it isn't an actual type. So we have to put all of its 621 * children in instead. 622 */ 623 if (valueDef.isStandardType()) { 624 ParametersUtil.addPart(myContext, operation, PARAMETER_VALUE, value); 625 } else { 626 for (BaseRuntimeChildDefinition nextChild : valueDef.getChildren()) { 627 List<IBase> childValues = nextChild.getAccessor().getValues(value); 628 for (int index = 0; index < childValues.size(); index++) { 629 boolean childRepeatable = theChildDefinition.getMax() != 1; 630 String elementName = nextChild.getChildNameByDatatype( 631 childValues.get(index).getClass()); 632 String targetPath = thePath + (childRepeatable ? "[" + index + "]" : "") + "." + elementName; 633 addInsertItems(theDiff, childValues, index, targetPath, nextChild); 634 } 635 } 636 } 637 } 638 639 private void addValueToDiff(IBase theOperationPart, IBase theOldValue, IBase theNewValue) { 640 641 if (myIncludePreviousValueInDiff) { 642 IBase oldValue = massageValueForDiff(theOldValue); 643 ParametersUtil.addPart(myContext, theOperationPart, "previousValue", oldValue); 644 } 645 646 IBase newValue = massageValueForDiff(theNewValue); 647 ParametersUtil.addPart(myContext, theOperationPart, PARAMETER_VALUE, newValue); 648 } 649 650 private boolean pathIsIgnored(EncodeContextPath theSourceEncodeContext) { 651 boolean pathIsIgnored = false; 652 for (EncodeContextPath next : myIgnorePaths) { 653 if (theSourceEncodeContext.startsWith(next, false)) { 654 pathIsIgnored = true; 655 break; 656 } 657 } 658 return pathIsIgnored; 659 } 660 661 private IBase massageValueForDiff(IBase theNewValue) { 662 IBase massagedValue = theNewValue; 663 664 // XHTML content is dealt with by putting it in a string 665 if (theNewValue instanceof XhtmlNode) { 666 String xhtmlString = ((XhtmlNode) theNewValue).getValueAsString(); 667 massagedValue = myContext.getElementDefinition("string").newInstance(xhtmlString); 668 } 669 670 // IIdType can hold a fully qualified ID, but we just want the ID part to show up in diffs 671 if (theNewValue instanceof IIdType) { 672 String idPart = ((IIdType) theNewValue).getIdPart(); 673 massagedValue = myContext.getElementDefinition("id").newInstance(idPart); 674 } 675 676 return massagedValue; 677 } 678 679 private String toValue(IPrimitiveType<?> theOldPrimitive) { 680 if (theOldPrimitive instanceof IIdType) { 681 return ((IIdType) theOldPrimitive).getIdPart(); 682 } 683 return theOldPrimitive.getValueAsString(); 684 } 685 686 private static class ChildDefinition { 687 private final BaseRuntimeChildDefinition myChildDef; 688 private final BaseRuntimeElementDefinition<?> myChildElement; 689 690 public ChildDefinition( 691 BaseRuntimeChildDefinition theChildDef, BaseRuntimeElementDefinition<?> theChildElement) { 692 this.myChildDef = theChildDef; 693 this.myChildElement = theChildElement; 694 } 695 696 public BaseRuntimeChildDefinition getChildDef() { 697 return myChildDef; 698 } 699 700 public BaseRuntimeElementDefinition<?> getChildElement() { 701 return myChildElement; 702 } 703 } 704 705 /** 706 * This class helps parse a FHIR path into its component parts for easier patch operation processing. 707 * It has 3 components: 708 * - The last element name, which is the last element in the path (not including any list index or filter) 709 * - The containing path, which is the prefix of the path up to the last element 710 * - A flag indicating whether the path has a filter or index on the last element of the path, which indicates 711 * that the path we are dealing is probably for a list element. 712 * Examples: 713 * 1. For path "Patient.identifier[2].system", 714 * - the lastElementName is "system", 715 * - the containingPath is "Patient.identifier[2]", 716 * - and endsWithAFilterOrIndex flag is false 717 * 718 * 2. For path "Patient.identifier[2]" or for path "Patient.identifier.where('system'='sys1')" 719 * - the lastElementName is "identifier", 720 * - the containingPath is "Patient", 721 * - and the endsWithAFilterOrIndex is true 722 */ 723 protected static class ParsedPath { 724 private final String myLastElementName; 725 private final String myContainingPath; 726 private final boolean myEndsWithAFilterOrIndex; 727 728 public ParsedPath(String theLastElementName, String theContainingPath, boolean theEndsWithAFilterOrIndex) { 729 myLastElementName = theLastElementName; 730 myContainingPath = theContainingPath; 731 myEndsWithAFilterOrIndex = theEndsWithAFilterOrIndex; 732 } 733 734 /** 735 * returns the last element of the path 736 */ 737 public String getLastElementName() { 738 return myLastElementName; 739 } 740 741 /** 742 * Returns the prefix of the path up to the last FHIR resource element 743 */ 744 public String getContainingPath() { 745 return myContainingPath; 746 } 747 748 /** 749 * Returns whether the path has a filter or index on the last element of the path, which indicates 750 * that the path we are dealing is probably a list element. 751 */ 752 public boolean getEndsWithAFilterOrIndex() { 753 return myEndsWithAFilterOrIndex; 754 } 755 756 public static ParsedPath parse(String path) { 757 String containingPath; 758 String elementName; 759 boolean endsWithAFilterOrIndex = false; 760 761 if (path.endsWith(")")) { 762 // This is probably a filter, so we're probably dealing with a list 763 endsWithAFilterOrIndex = true; 764 int filterArgsIndex = path.lastIndexOf('('); // Let's hope there aren't nested parentheses 765 int lastDotIndex = path.lastIndexOf( 766 '.', 767 filterArgsIndex); // There might be a dot inside the parentheses, so look to the left of that 768 int secondLastDotIndex = path.lastIndexOf('.', lastDotIndex - 1); 769 containingPath = path.substring(0, secondLastDotIndex); 770 elementName = path.substring(secondLastDotIndex + 1, lastDotIndex); 771 } else if (path.endsWith("]")) { 772 // This is almost definitely a list 773 endsWithAFilterOrIndex = true; 774 int openBracketIndex = path.lastIndexOf('['); 775 int lastDotIndex = path.lastIndexOf('.', openBracketIndex); 776 containingPath = path.substring(0, lastDotIndex); 777 elementName = path.substring(lastDotIndex + 1, openBracketIndex); 778 } else { 779 int lastDot = path.lastIndexOf("."); 780 containingPath = path.substring(0, lastDot); 781 elementName = path.substring(lastDot + 1); 782 } 783 784 return new ParsedPath(elementName, containingPath, endsWithAFilterOrIndex); 785 } 786 } 787}