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