
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.fhirpath.IFhirPath; 027import ca.uhn.fhir.i18n.Msg; 028import ca.uhn.fhir.jpa.util.FhirPathUtils; 029import ca.uhn.fhir.parser.path.EncodeContextPath; 030import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 031import ca.uhn.fhir.util.IModelVisitor2; 032import ca.uhn.fhir.util.ParametersUtil; 033import jakarta.annotation.Nonnull; 034import jakarta.annotation.Nullable; 035import org.apache.commons.lang3.StringUtils; 036import org.apache.commons.lang3.Validate; 037import org.hl7.fhir.instance.model.api.IBase; 038import org.hl7.fhir.instance.model.api.IBaseEnumeration; 039import org.hl7.fhir.instance.model.api.IBaseParameters; 040import org.hl7.fhir.instance.model.api.IBaseResource; 041import org.hl7.fhir.instance.model.api.IIdType; 042import org.hl7.fhir.instance.model.api.IPrimitiveType; 043import org.hl7.fhir.utilities.xhtml.XhtmlNode; 044 045import java.util.ArrayList; 046import java.util.Collections; 047import java.util.HashSet; 048import java.util.List; 049import java.util.Objects; 050import java.util.Optional; 051import java.util.Set; 052import java.util.Stack; 053import java.util.concurrent.atomic.AtomicReference; 054import java.util.function.Predicate; 055 056import static java.util.Objects.isNull; 057import static org.apache.commons.lang3.StringUtils.defaultString; 058import static org.apache.commons.lang3.StringUtils.isNotBlank; 059 060/** 061 * FhirPatch handler. 062 * Patch is defined by the spec: https://www.hl7.org/fhir/R4/fhirpatch.html 063 */ 064public class FhirPatch { 065 org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirPatch.class); 066 067 public static final String OPERATION_ADD = "add"; 068 public static final String OPERATION_DELETE = "delete"; 069 public static final String OPERATION_INSERT = "insert"; 070 public static final String OPERATION_MOVE = "move"; 071 public static final String OPERATION_REPLACE = "replace"; 072 public static final String PARAMETER_DESTINATION = "destination"; 073 public static final String PARAMETER_INDEX = "index"; 074 public static final String PARAMETER_NAME = "name"; 075 public static final String PARAMETER_OPERATION = "operation"; 076 public static final String PARAMETER_PATH = "path"; 077 public static final String PARAMETER_SOURCE = "source"; 078 public static final String PARAMETER_TYPE = "type"; 079 public static final String PARAMETER_VALUE = "value"; 080 081 private final FhirContext myContext; 082 private boolean myIncludePreviousValueInDiff; 083 private Set<EncodeContextPath> myIgnorePaths = Collections.emptySet(); 084 085 public FhirPatch(FhirContext theContext) { 086 myContext = theContext; 087 } 088 089 /** 090 * Adds a path element that will not be included in generated diffs. Values can take the form 091 * <code>ResourceName.fieldName.fieldName</code> and wildcards are supported, such 092 * as <code>*.meta</code>. 093 */ 094 public void addIgnorePath(String theIgnorePath) { 095 Validate.notBlank(theIgnorePath, "theIgnorePath must not be null or empty"); 096 097 if (myIgnorePaths.isEmpty()) { 098 myIgnorePaths = new HashSet<>(); 099 } 100 myIgnorePaths.add(new EncodeContextPath(theIgnorePath)); 101 } 102 103 public void setIncludePreviousValueInDiff(boolean theIncludePreviousValueInDiff) { 104 myIncludePreviousValueInDiff = theIncludePreviousValueInDiff; 105 } 106 107 public void apply(IBaseResource theResource, IBaseResource thePatch) { 108 109 List<IBase> opParameters = ParametersUtil.getNamedParameters(myContext, thePatch, PARAMETER_OPERATION); 110 for (IBase nextOperation : opParameters) { 111 String type = ParametersUtil.getParameterPartValueAsString(myContext, nextOperation, PARAMETER_TYPE); 112 type = defaultString(type); 113 114 if (OPERATION_DELETE.equals(type)) { 115 handleDeleteOperation(theResource, nextOperation); 116 } else if (OPERATION_ADD.equals(type)) { 117 handleAddOperation(theResource, nextOperation); 118 } else if (OPERATION_REPLACE.equals(type)) { 119 handleReplaceOperation(theResource, nextOperation); 120 } else if (OPERATION_INSERT.equals(type)) { 121 handleInsertOperation(theResource, nextOperation); 122 } else if (OPERATION_MOVE.equals(type)) { 123 handleMoveOperation(theResource, nextOperation); 124 } else { 125 throw new InvalidRequestException(Msg.code(1267) + "Unknown patch operation type: " + type); 126 } 127 } 128 } 129 130 private void handleAddOperation(IBaseResource theResource, IBase theParameters) { 131 132 String path = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_PATH); 133 String elementName = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_NAME); 134 135 String containingPath = defaultString(path); 136 137 List<IBase> containingElements = myContext.newFhirPath().evaluate(theResource, containingPath, IBase.class); 138 for (IBase nextElement : containingElements) { 139 ChildDefinition childDefinition = findChildDefinition(nextElement, elementName); 140 141 IBase newValue = getNewValue(theParameters, childDefinition); 142 143 childDefinition.getUseableChildDef().getMutator().addValue(nextElement, newValue); 144 } 145 } 146 147 private void handleInsertOperation(IBaseResource theResource, IBase theParameters) { 148 149 String path = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_PATH); 150 path = defaultString(path); 151 152 int lastDot = path.lastIndexOf("."); 153 String containingPath = path.substring(0, lastDot); 154 String elementName = path.substring(lastDot + 1); 155 Integer insertIndex = ParametersUtil.getParameterPartValueAsInteger(myContext, theParameters, PARAMETER_INDEX) 156 .orElseThrow(() -> new InvalidRequestException("No index supplied for insert operation")); 157 158 List<IBase> containingElements = myContext.newFhirPath().evaluate(theResource, containingPath, IBase.class); 159 for (IBase nextElement : containingElements) { 160 161 ChildDefinition childDefinition = findChildDefinition(nextElement, elementName); 162 163 IBase newValue = getNewValue(theParameters, childDefinition); 164 165 List<IBase> existingValues = new ArrayList<>( 166 childDefinition.getUseableChildDef().getAccessor().getValues(nextElement)); 167 if (insertIndex == null || insertIndex < 0 || insertIndex > existingValues.size()) { 168 String msg = myContext 169 .getLocalizer() 170 .getMessage(FhirPatch.class, "invalidInsertIndex", insertIndex, path, existingValues.size()); 171 throw new InvalidRequestException(Msg.code(1270) + msg); 172 } 173 existingValues.add(insertIndex, newValue); 174 175 childDefinition.getUseableChildDef().getMutator().setValue(nextElement, null); 176 for (IBase nextNewValue : existingValues) { 177 childDefinition.getUseableChildDef().getMutator().addValue(nextElement, nextNewValue); 178 } 179 } 180 } 181 182 private void handleDeleteOperation(IBaseResource theResource, IBase theParameters) { 183 String path = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_PATH); 184 path = defaultString(path); 185 186 ParsedPath parsedPath = ParsedPath.parse(path); 187 List<IBase> containingElements = myContext 188 .newFhirPath() 189 .evaluate( 190 theResource, 191 parsedPath.getEndsWithAFilterOrIndex() ? parsedPath.getContainingPath() : path, 192 IBase.class); 193 194 for (IBase nextElement : containingElements) { 195 if (parsedPath.getEndsWithAFilterOrIndex()) { 196 // if the path ends with a filter or index, we must be dealing with a list 197 deleteFromList(theResource, nextElement, parsedPath.getLastElementName(), path); 198 } else { 199 deleteSingleElement(nextElement); 200 } 201 } 202 } 203 204 private void deleteFromList( 205 IBaseResource theResource, 206 IBase theContainingElement, 207 String theListElementName, 208 String theElementToDeletePath) { 209 ChildDefinition childDefinition = findChildDefinition(theContainingElement, theListElementName); 210 211 List<IBase> existingValues = new ArrayList<>( 212 childDefinition.getUseableChildDef().getAccessor().getValues(theContainingElement)); 213 List<IBase> elementsToRemove = 214 myContext.newFhirPath().evaluate(theResource, theElementToDeletePath, IBase.class); 215 existingValues.removeAll(elementsToRemove); 216 217 childDefinition.getUseableChildDef().getMutator().setValue(theContainingElement, null); 218 for (IBase nextNewValue : existingValues) { 219 childDefinition.getUseableChildDef().getMutator().addValue(theContainingElement, nextNewValue); 220 } 221 } 222 223 private void handleReplaceOperation(IBaseResource theResource, IBase theParameters) { 224 String path = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_PATH); 225 path = defaultString(path); 226 227 ParsedFhirPath parsedFhirPath = ParsedFhirPath.parse(path); 228 IFhirPath fhirPath = myContext.newFhirPath(); 229 230 FhirPathChildDefinition parentDef = new FhirPathChildDefinition(); 231 232 List<ParsedFhirPath.FhirPathNode> pathNodes = new ArrayList<>(); 233 parsedFhirPath.getAllNodesWithPred(pathNodes, ParsedFhirPath.FhirPathNode::isNormalPathNode); 234 List<String> parts = new ArrayList<>(); 235 for (ParsedFhirPath.FhirPathNode node : pathNodes) { 236 parts.add(node.getValue()); 237 } 238 239 // fetch all runtime definitions along fhirpath 240 FhirPathChildDefinition cd = childDefinition(parentDef, parts, theResource, fhirPath, parsedFhirPath, path); 241 242 // replace the value 243 replaceValuesByPath(cd, theParameters, fhirPath, parsedFhirPath); 244 } 245 246 private void replaceValuesByPath( 247 FhirPathChildDefinition theChildDefinition, 248 IBase theParameters, 249 IFhirPath theFhirPath, 250 ParsedFhirPath theParsedFhirPath) { 251 Optional<IBase> singleValuePart = 252 ParametersUtil.getParameterPartValue(myContext, theParameters, PARAMETER_VALUE); 253 if (singleValuePart.isPresent()) { 254 IBase replacementValue = singleValuePart.get(); 255 256 FhirPathChildDefinition childDefinitionToUse = 257 findChildDefinitionByReplacementType(theChildDefinition, replacementValue); 258 259 // only a single replacement value (ie, not a replacement CompositeValue or anything) 260 replaceSingleValue(theFhirPath, theParsedFhirPath, childDefinitionToUse, replacementValue); 261 return; // guard 262 } 263 264 Optional<IBase> valueParts = ParametersUtil.getParameterPart(myContext, theParameters, PARAMETER_VALUE); 265 if (valueParts.isPresent()) { 266 // multiple replacement values provided via parts 267 List<IBase> partParts = valueParts.map(this::extractPartsFromPart).orElse(Collections.emptyList()); 268 269 for (IBase nextValuePartPart : partParts) { 270 String name = myContext 271 .newTerser() 272 .getSingleValue(nextValuePartPart, PARAMETER_NAME, IPrimitiveType.class) 273 .map(IPrimitiveType::getValueAsString) 274 .orElse(null); 275 276 if (StringUtils.isBlank(name)) { 277 continue; 278 } 279 280 Optional<IBase> optionalValue = 281 myContext.newTerser().getSingleValue(nextValuePartPart, "value[x]", IBase.class); 282 if (optionalValue.isPresent()) { 283 FhirPathChildDefinition childDefinitionToUse = 284 findChildDefinitionAtEndOfPath(theChildDefinition, nextValuePartPart); 285 286 BaseRuntimeChildDefinition subChild = 287 childDefinitionToUse.getElementDefinition().getChildByName(name); 288 289 subChild.getMutator().setValue(childDefinitionToUse.getBase(), optionalValue.get()); 290 } 291 } 292 293 return; // guard 294 } 295 296 // fall through to error state 297 throw new InvalidRequestException(Msg.code(2720) + " No valid replacement value for patch operation."); 298 } 299 300 private FhirPathChildDefinition findChildDefinitionByReplacementType( 301 FhirPathChildDefinition theChildDefinition, IBase replacementValue) { 302 boolean isPrimitive = replacementValue instanceof IPrimitiveType<?>; 303 Predicate<FhirPathChildDefinition> predicate = def -> { 304 if (isPrimitive) { 305 // primitives will be at the very bottom (ie, no children underneath) 306 return def.getBase() instanceof IPrimitiveType<?>; 307 } else if (def.getBase().fhirType().equalsIgnoreCase(replacementValue.fhirType())) { 308 return true; 309 } 310 return false; 311 }; 312 313 return findChildDefinition(theChildDefinition, predicate); 314 } 315 316 private FhirPathChildDefinition findChildDefinitionAtEndOfPath( 317 FhirPathChildDefinition theChildDefinition, IBase replacementValue) { 318 return findChildDefinition(theChildDefinition, childDefinition -> { 319 return childDefinition.getChild() == null; 320 }); 321 } 322 323 private FhirPathChildDefinition findChildDefinition( 324 FhirPathChildDefinition theChildDefinition, Predicate<FhirPathChildDefinition> thePredicate) { 325 FhirPathChildDefinition childDefinitionToUse = theChildDefinition; 326 while (childDefinitionToUse != null) { 327 if (thePredicate.test(childDefinitionToUse)) { 328 return childDefinitionToUse; 329 } 330 childDefinitionToUse = childDefinitionToUse.getChild(); 331 } 332 333 throw new InvalidRequestException(Msg.code(2719) + " No runtime definition found for patch operation."); 334 } 335 336 private void replaceSingleValue( 337 IFhirPath theFhirPath, 338 ParsedFhirPath theParsedFhirPath, 339 FhirPathChildDefinition theTargetChildDefinition, 340 IBase theReplacementValue) { 341 if (theTargetChildDefinition.getElementDefinition().getChildType() 342 == BaseRuntimeElementDefinition.ChildTypeEnum.PRIMITIVE_DATATYPE) { 343 if (theTargetChildDefinition.getBase() instanceof IPrimitiveType<?> target 344 && theReplacementValue instanceof IPrimitiveType<?> source) { 345 if (target.fhirType().equalsIgnoreCase(source.fhirType())) { 346 if (theTargetChildDefinition 347 .getParent() 348 .getBase() 349 .fhirType() 350 .equalsIgnoreCase("narrative") 351 && theTargetChildDefinition.getFhirPath().equalsIgnoreCase("div")) { 352 /* 353 * Special case handling for Narrative elements 354 * because xhtml is a primitive type, but it's fhirtype is recorded as "string" 355 * (which means we cannot actually assign it as a primitive type). 356 * 357 * Instead, we have to get the parent's type and set it's child as a new 358 * XHTML child. 359 */ 360 FhirPathChildDefinition narrativeDefinition = theTargetChildDefinition.getParent(); 361 BaseRuntimeElementDefinition<?> narrativeElement = narrativeDefinition.getElementDefinition(); 362 363 BaseRuntimeElementDefinition<?> newXhtmlEl = myContext.getElementDefinition("xhtml"); 364 365 IPrimitiveType<?> xhtmlType; 366 if (theTargetChildDefinition.getBaseRuntimeDefinition().getInstanceConstructorArguments() 367 != null) { 368 xhtmlType = (IPrimitiveType<?>) newXhtmlEl.newInstance(theTargetChildDefinition 369 .getBaseRuntimeDefinition() 370 .getInstanceConstructorArguments()); 371 } else { 372 xhtmlType = (IPrimitiveType<?>) newXhtmlEl.newInstance(); 373 } 374 375 xhtmlType.setValueAsString(source.getValueAsString()); 376 narrativeElement 377 .getChildByName(theTargetChildDefinition.getFhirPath()) 378 .getMutator() 379 .setValue(narrativeDefinition.getBase(), xhtmlType); 380 } else { 381 target.setValueAsString(source.getValueAsString()); 382 } 383 } else if (theTargetChildDefinition.getChild() != null) { 384 // there's subchildren (possibly we're setting an 'extension' value 385 FhirPathChildDefinition ct = 386 findChildDefinitionAtEndOfPath(theTargetChildDefinition, theReplacementValue); 387 replaceSingleValue(theFhirPath, theParsedFhirPath, ct, theReplacementValue); 388 } else { 389 if (theTargetChildDefinition.getBaseRuntimeDefinition() != null 390 && !theTargetChildDefinition 391 .getBaseRuntimeDefinition() 392 .isMultipleCardinality()) { 393 // basic primitive type assignment 394 target.setValueAsString(source.getValueAsString()); 395 return; 396 } 397 398 // the primitive can have multiple value types 399 BaseRuntimeElementDefinition<?> parentEl = 400 theTargetChildDefinition.getParent().getElementDefinition(); 401 String childFhirPath = theTargetChildDefinition.getFhirPath(); 402 403 BaseRuntimeChildDefinition choiceTarget = parentEl.getChildByName(childFhirPath); 404 if (choiceTarget == null) { 405 // possibly a choice type 406 choiceTarget = parentEl.getChildByName(childFhirPath + "[x]"); 407 } 408 choiceTarget 409 .getMutator() 410 .setValue(theTargetChildDefinition.getParent().getBase(), theReplacementValue); 411 } 412 } 413 return; 414 } 415 416 IBase containingElement = theTargetChildDefinition.getParent().getBase(); 417 BaseRuntimeChildDefinition runtimeDef = theTargetChildDefinition.getBaseRuntimeDefinition(); 418 if (runtimeDef == null) { 419 runtimeDef = theTargetChildDefinition.getParent().getBaseRuntimeDefinition(); 420 } 421 422 if (runtimeDef.isMultipleCardinality()) { 423 // a list 424 List<IBase> existing = new ArrayList<>(runtimeDef.getAccessor().getValues(containingElement)); 425 if (existing.isEmpty()) { 426 // no elements to replace - we shouldn't see this here though 427 String msg = myContext 428 .getLocalizer() 429 .getMessage(FhirPatch.class, "noMatchingElementForPath", theParsedFhirPath.getRawPath()); 430 throw new InvalidRequestException(Msg.code(2617) + msg); 431 } 432 433 List<IBase> replaceables; 434 if (FhirPathUtils.isSubsettingNode(theParsedFhirPath.getTail())) { 435 replaceables = applySubsettingFilter(theParsedFhirPath, theParsedFhirPath.getTail(), existing); 436 } else if (existing.size() == 1) { 437 replaceables = existing; 438 } else { 439 String raw = theParsedFhirPath.getRawPath(); 440 String finalNode = theParsedFhirPath.getLastElementName(); 441 String subpath = raw.substring(raw.indexOf(finalNode)); 442 if (subpath.startsWith(finalNode) && subpath.length() > finalNode.length()) { 443 subpath = subpath.substring(finalNode.length() + 1); // + 1 for the "." 444 } 445 446 AtomicReference<String> subpathRef = new AtomicReference<>(); 447 subpathRef.set(subpath); 448 replaceables = existing.stream() 449 .filter(item -> { 450 Optional<IBase> matched = theFhirPath.evaluateFirst(item, subpathRef.get(), IBase.class); 451 return matched.isPresent(); 452 }) 453 .toList(); 454 } 455 456 if (replaceables.size() != 1) { 457 throw new InvalidRequestException( 458 Msg.code(2715) + " Expected to find a single element, but provided FhirPath returned " 459 + replaceables.size() + " elements."); 460 } 461 IBase valueToReplace = replaceables.get(0); 462 463 BaseRuntimeChildDefinition.IMutator listMutator = runtimeDef.getMutator(); 464 // clear the whole list first, then reconstruct it in the loop below replacing the values that need to be 465 // replaced 466 listMutator.setValue(containingElement, null); 467 for (IBase existingValue : existing) { 468 if (valueToReplace.equals(existingValue)) { 469 listMutator.addValue(containingElement, theReplacementValue); 470 } else { 471 listMutator.addValue(containingElement, existingValue); 472 } 473 } 474 } else { 475 // a single element 476 runtimeDef.getMutator().setValue(containingElement, theReplacementValue); 477 } 478 } 479 480 private List<IBase> applySubsettingFilter( 481 ParsedFhirPath theParsed, ParsedFhirPath.FhirPathNode tail, List<IBase> filtered) { 482 if (tail.getListIndex() >= 0) { 483 // specific index 484 if (tail.getListIndex() < filtered.size()) { 485 return List.of(filtered.get(tail.getListIndex())); 486 } else { 487 ourLog.info("Nothing matching index {}; nothing patched.", tail.getListIndex()); 488 return List.of(); 489 } 490 } else { 491 if (filtered.isEmpty()) { 492 // empty lists should match all filters, so we'll return it here 493 ourLog.info("List contains no elements; no patching will occur"); 494 return List.of(); 495 } 496 497 switch (tail.getValue()) { 498 case "first" -> { 499 return List.of(filtered.get(0)); 500 } 501 case "last" -> { 502 return List.of(filtered.get(filtered.size() - 1)); 503 } 504 case "tail" -> { 505 if (filtered.size() == 1) { 506 ourLog.info("List contains only a single element - no patching will occur"); 507 return List.of(); 508 } 509 return filtered.subList(1, filtered.size()); 510 } 511 case "single" -> { 512 if (filtered.size() != 1) { 513 throw new InvalidRequestException( 514 Msg.code(2710) + " List contains more than a single element."); 515 } 516 // only one element 517 return filtered; 518 } 519 case "skip", "take" -> { 520 if (tail instanceof ParsedFhirPath.FhirPathFunction fn) { 521 String containedNum = fn.getContainedExp().getHead().getValue(); 522 try { 523 int num = Integer.parseInt(containedNum); 524 525 if (tail.getValue().equals("skip")) { 526 if (num < filtered.size()) { 527 return filtered.subList(num, filtered.size()); 528 } 529 } else if (tail.getValue().equals("take")) { 530 if (num < filtered.size()) { 531 return filtered.subList(0, num); 532 } else { 533 // otherwise, return everything 534 return filtered; 535 } 536 } 537 538 return List.of(); 539 } catch (NumberFormatException ex) { 540 ourLog.error("{} is not a number", containedNum, ex); 541 } 542 } 543 throw new InvalidRequestException( 544 Msg.code(2712) + " Invalid fhir path element encountered: " + theParsed.getRawPath()); 545 } 546 default -> { 547 // we shouldn't see this; it means we have not handled a filtering case 548 throw new InvalidRequestException( 549 Msg.code(2711) + " Unrecognized filter of type " + tail.getValue()); 550 } 551 } 552 } 553 } 554 555 private void throwNoElementsError(String theFullReplacePath) { 556 String msg = 557 myContext.getLocalizer().getMessage(FhirPatch.class, "noMatchingElementForPath", theFullReplacePath); 558 throw new InvalidRequestException(Msg.code(2617) + msg); 559 } 560 561 private void handleMoveOperation(IBaseResource theResource, IBase theParameters) { 562 String path = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_PATH); 563 path = defaultString(path); 564 565 int lastDot = path.lastIndexOf("."); 566 String containingPath = path.substring(0, lastDot); 567 String elementName = path.substring(lastDot + 1); 568 Integer insertIndex = ParametersUtil.getParameterPartValueAsInteger( 569 myContext, theParameters, PARAMETER_DESTINATION) 570 .orElseThrow(() -> new InvalidRequestException("No index supplied for move operation")); 571 Integer removeIndex = ParametersUtil.getParameterPartValueAsInteger(myContext, theParameters, PARAMETER_SOURCE) 572 .orElseThrow(() -> new InvalidRequestException("No index supplied for move operation")); 573 574 List<IBase> containingElements = myContext.newFhirPath().evaluate(theResource, containingPath, IBase.class); 575 for (IBase nextElement : containingElements) { 576 577 ChildDefinition childDefinition = findChildDefinition(nextElement, elementName); 578 579 List<IBase> existingValues = new ArrayList<>( 580 childDefinition.getUseableChildDef().getAccessor().getValues(nextElement)); 581 if (removeIndex == null || removeIndex < 0 || removeIndex >= existingValues.size()) { 582 String msg = myContext 583 .getLocalizer() 584 .getMessage( 585 FhirPatch.class, "invalidMoveSourceIndex", removeIndex, path, existingValues.size()); 586 throw new InvalidRequestException(Msg.code(1268) + msg); 587 } 588 IBase newValue = existingValues.remove(removeIndex.intValue()); 589 590 if (insertIndex == null || insertIndex < 0 || insertIndex > existingValues.size()) { 591 String msg = myContext 592 .getLocalizer() 593 .getMessage( 594 FhirPatch.class, 595 "invalidMoveDestinationIndex", 596 insertIndex, 597 path, 598 existingValues.size()); 599 throw new InvalidRequestException(Msg.code(1269) + msg); 600 } 601 existingValues.add(insertIndex, newValue); 602 603 childDefinition.getUseableChildDef().getMutator().setValue(nextElement, null); 604 for (IBase nextNewValue : existingValues) { 605 childDefinition.getUseableChildDef().getMutator().addValue(nextElement, nextNewValue); 606 } 607 } 608 } 609 610 private FhirPathChildDefinition childDefinition( 611 FhirPathChildDefinition theParent, 612 List<String> theFhirPathParts, 613 IBase theBase, 614 IFhirPath theFhirPath, 615 ParsedFhirPath theParsedFhirPath, 616 String theOriginalPath) { 617 FhirPathChildDefinition definition = new FhirPathChildDefinition(); 618 definition.setBase(theBase); // set this IBase value 619 BaseRuntimeElementDefinition<?> parentElementDefinition = myContext.getElementDefinition(theBase.getClass()); 620 definition.setElementDefinition(parentElementDefinition); // set this element 621 622 String head = theParsedFhirPath.getHead().getValue(); 623 definition.setFhirPath(head); 624 625 if (theParent.getElementDefinition() != null) { 626 definition.setBaseRuntimeDefinition(theParent.getElementDefinition().getChildByName(head)); 627 } 628 629 String rawPath = theParsedFhirPath.getRawPath(); 630 631 if (rawPath.equalsIgnoreCase(head)) { 632 // we're at the bottom 633 // return 634 return definition; 635 } 636 637 // detach the head 638 String headVal = theFhirPathParts.remove(0); 639 String pathBeneathParent = rawPath.substring(headVal.length()); 640 pathBeneathParent = FhirPathUtils.cleansePath(pathBeneathParent); 641 642 if (isNotBlank(pathBeneathParent) && !theFhirPathParts.isEmpty()) { 643 Stack<ParsedFhirPath.FhirPathNode> filteringNodes = new Stack<>(); 644 645 String childFilteringPath = pathBeneathParent; 646 String nextPath = pathBeneathParent; 647 648 if (FhirPathUtils.isSubsettingNode(theParsedFhirPath.getTail())) { 649 // the final node in this path is .first() or [0]... etc 650 ParsedFhirPath.FhirPathNode filteringNode = theParsedFhirPath.getTail(); 651 filteringNodes.push(filteringNode); 652 /* 653 * the field filtering path will be the path - tail value. 654 * This will also be nextPath (the one we recurse on) 655 */ 656 int endInd = pathBeneathParent.indexOf(filteringNode.getValue()); 657 if (endInd == -1) { 658 endInd = pathBeneathParent.length(); 659 } 660 childFilteringPath = pathBeneathParent.substring(0, endInd); 661 childFilteringPath = FhirPathUtils.cleansePath(childFilteringPath); 662 nextPath = childFilteringPath; 663 } 664 665 String directChildName = theFhirPathParts.get(0); 666 667 ParsedFhirPath newPath = ParsedFhirPath.parse(nextPath); 668 669 if (newPath.getHead() instanceof ParsedFhirPath.FhirPathFunction fn && fn.hasContainedExp()) { 670 newPath = fn.getContainedExp(); 671 672 childFilteringPath = newPath.getRawPath(); 673 } 674 675 // get all direct children 676 ParsedFhirPath.FhirPathNode newHead = newPath.getHead(); 677 List<IBase> allChildren = theFhirPath.evaluate(theBase, directChildName, IBase.class); 678 679 // go through the children and take only the ones that match the path we have 680 String filterPath = childFilteringPath; 681 682 List<IBase> childs; 683 if (filterPath.startsWith(newHead.getValue()) && !filterPath.equalsIgnoreCase(newHead.getValue())) { 684 filterPath = filterPath.substring(newHead.getValue().length()); 685 filterPath = FhirPathUtils.cleansePath(filterPath); 686 687 if (newPath.getHead().getNext() != null 688 && FhirPathUtils.isSubsettingNode(newPath.getHead().getNext())) { 689 // yet another filter node 690 ParsedFhirPath.FhirPathNode filterNode = newPath.getHead().getNext(); 691 filteringNodes.push(filterNode); 692 693 String newRaw = newPath.getRawPath(); 694 String updated = ""; 695 if (filterNode.hasNext()) { 696 updated = newRaw.substring( 697 newRaw.indexOf(filterNode.getNext().getValue())); 698 updated = FhirPathUtils.cleansePath(updated); 699 } 700 filterPath = updated; 701 updated = newPath.getHead().getValue() + "." + updated; 702 newPath = ParsedFhirPath.parse(updated); 703 } 704 } 705 706 if (isNotBlank(filterPath)) { 707 if (theFhirPathParts.contains(filterPath)) { 708 /* 709 * We're filtering on just a fhirpath node for some reason (ie, "identifier" or "reference" or "string"). 710 * 711 * We don't need to apply the filter; 712 * all children should be the same as this filtered type 713 * (likely we have a subsetting filter to apply) 714 */ 715 childs = allChildren; 716 } else { 717 AtomicReference<String> ref = new AtomicReference<>(); 718 ref.set(filterPath); 719 if (allChildren.size() > 1) { 720 childs = allChildren.stream() 721 .filter(el -> { 722 Optional<IBase> match = theFhirPath.evaluateFirst(el, ref.get(), IBase.class); 723 return match.isPresent(); 724 }) 725 .toList(); 726 } else { 727 // there is only 1 child (probably a top level element) 728 // we still filter own because child elements can have different child types (that might match 729 // multiple childs) 730 // eg: everything has "extension" on it 731 childs = allChildren.stream() 732 .filter(el -> { 733 Optional<IBase> match = theFhirPath.evaluateFirst(el, ref.get(), IBase.class); 734 return match.isPresent(); 735 }) 736 .findFirst() 737 .stream() 738 .toList(); 739 } 740 } 741 } else { 742 childs = allChildren; 743 } 744 745 while (!filteringNodes.empty()) { 746 ParsedFhirPath.FhirPathNode filteringNode = filteringNodes.pop(); 747 childs = applySubsettingFilter(newPath, filteringNode, childs); 748 } 749 750 // should only be one 751 if (childs.size() != 1) { 752 if (childs.isEmpty()) { 753 throwNoElementsError(theOriginalPath); 754 } 755 throw new InvalidRequestException(Msg.code(2704) 756 + " FhirPath returns more than 1 element. Patch cannot be done. " + theOriginalPath); 757 } 758 IBase child = childs.get(0); 759 760 definition.setChild( 761 childDefinition(definition, theFhirPathParts, child, theFhirPath, newPath, theOriginalPath)); 762 } 763 764 return definition; 765 } 766 767 private ChildDefinition findChildDefinition(IBase theContainingElement, String theElementName) { 768 BaseRuntimeElementDefinition<?> elementDef = myContext.getElementDefinition(theContainingElement.getClass()); 769 770 String childName = theElementName; 771 BaseRuntimeChildDefinition childDef = elementDef.getChildByName(childName); 772 BaseRuntimeElementDefinition<?> childElement; 773 if (childDef == null) { 774 childName = theElementName + "[x]"; 775 childDef = elementDef.getChildByName(childName); 776 childElement = childDef.getChildByName( 777 childDef.getValidChildNames().iterator().next()); 778 } else { 779 childElement = childDef.getChildByName(childName); 780 } 781 782 return new ChildDefinition(childDef, childElement); 783 } 784 785 private IBase getNewValue(IBase theParameters, ChildDefinition theChildDefinition) { 786 Optional<IBase> valuePart = ParametersUtil.getParameterPart(myContext, theParameters, PARAMETER_VALUE); 787 Optional<IBase> valuePartValue = 788 ParametersUtil.getParameterPartValue(myContext, theParameters, PARAMETER_VALUE); 789 790 IBase newValue; 791 if (valuePartValue.isPresent()) { 792 newValue = maybeMassageToEnumeration(valuePartValue.get(), theChildDefinition); 793 794 } else { 795 List<IBase> partParts = valuePart.map(this::extractPartsFromPart).orElse(Collections.emptyList()); 796 797 newValue = createAndPopulateNewElement(theChildDefinition, partParts); 798 } 799 800 return newValue; 801 } 802 803 private IBase maybeMassageToEnumeration(IBase theValue, ChildDefinition theChildDefinition) { 804 IBase retVal = theValue; 805 806 if (IBaseEnumeration.class.isAssignableFrom( 807 theChildDefinition.getChildElement().getImplementingClass()) 808 || XhtmlNode.class.isAssignableFrom( 809 theChildDefinition.getChildElement().getImplementingClass())) { 810 // If the compositeElementDef is an IBaseEnumeration, we will use the actual compositeElementDef definition 811 // to build one, since 812 // it needs the right factory object passed to its constructor 813 IPrimitiveType<?> newValueInstance; 814 if (theChildDefinition.getUseableChildDef().getInstanceConstructorArguments() != null) { 815 newValueInstance = (IPrimitiveType<?>) theChildDefinition 816 .getChildElement() 817 .newInstance(theChildDefinition.getUseableChildDef().getInstanceConstructorArguments()); 818 } else { 819 newValueInstance = 820 (IPrimitiveType<?>) theChildDefinition.getChildElement().newInstance(); 821 } 822 newValueInstance.setValueAsString(((IPrimitiveType<?>) theValue).getValueAsString()); 823 retVal = newValueInstance; 824 } 825 826 return retVal; 827 } 828 829 @Nonnull 830 private List<IBase> extractPartsFromPart(IBase theParametersParameterComponent) { 831 return myContext.newTerser().getValues(theParametersParameterComponent, "part"); 832 } 833 834 /** 835 * this method will instantiate an element according to the provided Definition and initialize its fields 836 * from the values provided in thePartParts. a part usually represent a datatype as a name/value[X] pair. 837 * it may also represent a complex type like an Extension. 838 * 839 * @param theDefinition wrapper around the runtime definition of the element to be populated 840 * @param thePartParts list of Part to populate the element that will be created from theDefinition 841 * @return an element that was created from theDefinition and populated with the parts 842 */ 843 private IBase createAndPopulateNewElement(ChildDefinition theDefinition, List<IBase> thePartParts) { 844 IBase newElement = theDefinition.getChildElement().newInstance(); 845 846 for (IBase nextValuePartPart : thePartParts) { 847 String name = myContext 848 .newTerser() 849 .getSingleValue(nextValuePartPart, PARAMETER_NAME, IPrimitiveType.class) 850 .map(IPrimitiveType::getValueAsString) 851 .orElse(null); 852 853 if (StringUtils.isBlank(name)) { 854 continue; 855 } 856 857 Optional<IBase> optionalValue = 858 myContext.newTerser().getSingleValue(nextValuePartPart, "value[x]", IBase.class); 859 860 if (optionalValue.isPresent()) { 861 // we have a dataType. let's extract its value and assign it. 862 ChildDefinition childDefinition; 863 childDefinition = findChildDefinition(newElement, name); 864 865 IBase newValue = maybeMassageToEnumeration(optionalValue.get(), childDefinition); 866 867 BaseRuntimeChildDefinition partChildDef = 868 theDefinition.getUsableChildElement().getChildByName(name); 869 870 if (isNull(partChildDef)) { 871 name = name + "[x]"; 872 partChildDef = theDefinition.getUsableChildElement().getChildByName(name); 873 } 874 875 partChildDef.getMutator().setValue(newElement, newValue); 876 877 // a part represent a datatype or a complexType but not both at the same time. 878 continue; 879 } 880 881 List<IBase> part = extractPartsFromPart(nextValuePartPart); 882 883 if (!part.isEmpty()) { 884 // we have a complexType. let's find its definition and recursively process 885 // them till all complexTypes are processed. 886 ChildDefinition childDefinition = findChildDefinition(newElement, name); 887 888 IBase childNewValue = createAndPopulateNewElement(childDefinition, part); 889 890 childDefinition.getUseableChildDef().getMutator().setValue(newElement, childNewValue); 891 } 892 } 893 894 return newElement; 895 } 896 897 private void deleteSingleElement(IBase theElementToDelete) { 898 myContext.newTerser().visit(theElementToDelete, new IModelVisitor2() { 899 @Override 900 public boolean acceptElement( 901 IBase theElement, 902 List<IBase> theContainingElementPath, 903 List<BaseRuntimeChildDefinition> theChildDefinitionPath, 904 List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) { 905 if (theElement instanceof IPrimitiveType) { 906 ((IPrimitiveType<?>) theElement).setValueAsString(null); 907 } 908 return true; 909 } 910 }); 911 } 912 913 public IBaseParameters diff(@Nullable IBaseResource theOldValue, @Nonnull IBaseResource theNewValue) { 914 IBaseParameters retVal = ParametersUtil.newInstance(myContext); 915 String newValueTypeName = myContext.getResourceDefinition(theNewValue).getName(); 916 917 if (theOldValue == null) { 918 IBase operation = ParametersUtil.addParameterToParameters(myContext, retVal, PARAMETER_OPERATION); 919 ParametersUtil.addPartCode(myContext, operation, PARAMETER_TYPE, OPERATION_INSERT); 920 ParametersUtil.addPartString(myContext, operation, PARAMETER_PATH, newValueTypeName); 921 ParametersUtil.addPart(myContext, operation, PARAMETER_VALUE, theNewValue); 922 } else { 923 String oldValueTypeName = 924 myContext.getResourceDefinition(theOldValue).getName(); 925 Validate.isTrue(oldValueTypeName.equalsIgnoreCase(newValueTypeName), "Resources must be of same type"); 926 927 BaseRuntimeElementCompositeDefinition<?> def = 928 myContext.getResourceDefinition(theOldValue).getBaseDefinition(); 929 String path = def.getName(); 930 931 EncodeContextPath contextPath = new EncodeContextPath(); 932 contextPath.pushPath(path, true); 933 934 compare(retVal, contextPath, def, path, path, theOldValue, theNewValue); 935 936 contextPath.popPath(); 937 assert contextPath.getPath().isEmpty(); 938 } 939 940 return retVal; 941 } 942 943 private void compare( 944 IBaseParameters theDiff, 945 EncodeContextPath theSourceEncodeContext, 946 BaseRuntimeElementDefinition<?> theDef, 947 String theSourcePath, 948 String theTargetPath, 949 IBase theOldField, 950 IBase theNewField) { 951 952 boolean pathIsIgnored = pathIsIgnored(theSourceEncodeContext); 953 if (pathIsIgnored) { 954 return; 955 } 956 957 BaseRuntimeElementDefinition<?> sourceDef = myContext.getElementDefinition(theOldField.getClass()); 958 BaseRuntimeElementDefinition<?> targetDef = myContext.getElementDefinition(theNewField.getClass()); 959 if (!sourceDef.getName().equals(targetDef.getName())) { 960 IBase operation = ParametersUtil.addParameterToParameters(myContext, theDiff, PARAMETER_OPERATION); 961 ParametersUtil.addPartCode(myContext, operation, PARAMETER_TYPE, OPERATION_REPLACE); 962 ParametersUtil.addPartString(myContext, operation, PARAMETER_PATH, theTargetPath); 963 addValueToDiff(operation, theOldField, theNewField); 964 } else { 965 if (theOldField instanceof IPrimitiveType) { 966 IPrimitiveType<?> oldPrimitive = (IPrimitiveType<?>) theOldField; 967 IPrimitiveType<?> newPrimitive = (IPrimitiveType<?>) theNewField; 968 String oldValueAsString = toValue(oldPrimitive); 969 String newValueAsString = toValue(newPrimitive); 970 if (!Objects.equals(oldValueAsString, newValueAsString)) { 971 IBase operation = ParametersUtil.addParameterToParameters(myContext, theDiff, PARAMETER_OPERATION); 972 ParametersUtil.addPartCode(myContext, operation, PARAMETER_TYPE, OPERATION_REPLACE); 973 ParametersUtil.addPartString(myContext, operation, PARAMETER_PATH, theTargetPath); 974 addValueToDiff(operation, oldPrimitive, newPrimitive); 975 } 976 } 977 978 List<BaseRuntimeChildDefinition> children = theDef.getChildren(); 979 for (BaseRuntimeChildDefinition nextChild : children) { 980 compareField( 981 theDiff, 982 theSourceEncodeContext, 983 theSourcePath, 984 theTargetPath, 985 theOldField, 986 theNewField, 987 nextChild); 988 } 989 } 990 } 991 992 private void compareField( 993 IBaseParameters theDiff, 994 EncodeContextPath theSourceEncodePath, 995 String theSourcePath, 996 String theTargetPath, 997 IBase theOldField, 998 IBase theNewField, 999 BaseRuntimeChildDefinition theChildDef) { 1000 String elementName = theChildDef.getElementName(); 1001 boolean repeatable = theChildDef.getMax() != 1; 1002 theSourceEncodePath.pushPath(elementName, false); 1003 if (pathIsIgnored(theSourceEncodePath)) { 1004 theSourceEncodePath.popPath(); 1005 return; 1006 } 1007 1008 List<? extends IBase> sourceValues = theChildDef.getAccessor().getValues(theOldField); 1009 List<? extends IBase> targetValues = theChildDef.getAccessor().getValues(theNewField); 1010 1011 int sourceIndex = 0; 1012 int targetIndex = 0; 1013 while (sourceIndex < sourceValues.size() && targetIndex < targetValues.size()) { 1014 1015 IBase sourceChildField = sourceValues.get(sourceIndex); 1016 Validate.notNull(sourceChildField); // not expected to happen, but just in case 1017 BaseRuntimeElementDefinition<?> def = myContext.getElementDefinition(sourceChildField.getClass()); 1018 IBase targetChildField = targetValues.get(targetIndex); 1019 Validate.notNull(targetChildField); // not expected to happen, but just in case 1020 String sourcePath = theSourcePath + "." + elementName + (repeatable ? "[" + sourceIndex + "]" : ""); 1021 String targetPath = theSourcePath + "." + elementName + (repeatable ? "[" + targetIndex + "]" : ""); 1022 1023 compare(theDiff, theSourceEncodePath, def, sourcePath, targetPath, sourceChildField, targetChildField); 1024 1025 sourceIndex++; 1026 targetIndex++; 1027 } 1028 1029 // Find newly inserted items 1030 while (targetIndex < targetValues.size()) { 1031 String path = theTargetPath + "." + elementName; 1032 addInsertItems(theDiff, targetValues, targetIndex, path, theChildDef); 1033 targetIndex++; 1034 } 1035 1036 // Find deleted items 1037 while (sourceIndex < sourceValues.size()) { 1038 IBase operation = ParametersUtil.addParameterToParameters(myContext, theDiff, PARAMETER_OPERATION); 1039 ParametersUtil.addPartCode(myContext, operation, PARAMETER_TYPE, OPERATION_DELETE); 1040 ParametersUtil.addPartString( 1041 myContext, 1042 operation, 1043 PARAMETER_PATH, 1044 theTargetPath + "." + elementName + (repeatable ? "[" + targetIndex + "]" : "")); 1045 1046 sourceIndex++; 1047 targetIndex++; 1048 } 1049 1050 theSourceEncodePath.popPath(); 1051 } 1052 1053 private void addInsertItems( 1054 IBaseParameters theDiff, 1055 List<? extends IBase> theTargetValues, 1056 int theTargetIndex, 1057 String thePath, 1058 BaseRuntimeChildDefinition theChildDefinition) { 1059 IBase operation = ParametersUtil.addParameterToParameters(myContext, theDiff, PARAMETER_OPERATION); 1060 ParametersUtil.addPartCode(myContext, operation, PARAMETER_TYPE, OPERATION_INSERT); 1061 ParametersUtil.addPartString(myContext, operation, PARAMETER_PATH, thePath); 1062 ParametersUtil.addPartInteger(myContext, operation, PARAMETER_INDEX, theTargetIndex); 1063 1064 IBase value = theTargetValues.get(theTargetIndex); 1065 BaseRuntimeElementDefinition<?> valueDef = myContext.getElementDefinition(value.getClass()); 1066 1067 /* 1068 * If the value is a Resource or a datatype, we can put it into the part.value and that will cover 1069 * all of its children. If it's an infrastructure element though, such as Patient.contact we can't 1070 * just put it into part.value because it isn't an actual type. So we have to put all of its 1071 * children in instead. 1072 */ 1073 if (valueDef.isStandardType()) { 1074 ParametersUtil.addPart(myContext, operation, PARAMETER_VALUE, value); 1075 } else { 1076 for (BaseRuntimeChildDefinition nextChild : valueDef.getChildren()) { 1077 List<IBase> childValues = nextChild.getAccessor().getValues(value); 1078 for (int index = 0; index < childValues.size(); index++) { 1079 boolean childRepeatable = theChildDefinition.getMax() != 1; 1080 String elementName = nextChild.getChildNameByDatatype( 1081 childValues.get(index).getClass()); 1082 String targetPath = thePath + (childRepeatable ? "[" + index + "]" : "") + "." + elementName; 1083 addInsertItems(theDiff, childValues, index, targetPath, nextChild); 1084 } 1085 } 1086 } 1087 } 1088 1089 private void addValueToDiff(IBase theOperationPart, IBase theOldValue, IBase theNewValue) { 1090 1091 if (myIncludePreviousValueInDiff) { 1092 IBase oldValue = massageValueForDiff(theOldValue); 1093 ParametersUtil.addPart(myContext, theOperationPart, "previousValue", oldValue); 1094 } 1095 1096 IBase newValue = massageValueForDiff(theNewValue); 1097 ParametersUtil.addPart(myContext, theOperationPart, PARAMETER_VALUE, newValue); 1098 } 1099 1100 private boolean pathIsIgnored(EncodeContextPath theSourceEncodeContext) { 1101 boolean pathIsIgnored = false; 1102 for (EncodeContextPath next : myIgnorePaths) { 1103 if (theSourceEncodeContext.startsWith(next, false)) { 1104 pathIsIgnored = true; 1105 break; 1106 } 1107 } 1108 return pathIsIgnored; 1109 } 1110 1111 private IBase massageValueForDiff(IBase theNewValue) { 1112 IBase massagedValue = theNewValue; 1113 1114 // XHTML content is dealt with by putting it in a string 1115 if (theNewValue instanceof XhtmlNode) { 1116 String xhtmlString = ((XhtmlNode) theNewValue).getValueAsString(); 1117 massagedValue = myContext.getElementDefinition("string").newInstance(xhtmlString); 1118 } 1119 1120 // IIdType can hold a fully qualified ID, but we just want the ID part to show up in diffs 1121 if (theNewValue instanceof IIdType) { 1122 String idPart = ((IIdType) theNewValue).getIdPart(); 1123 massagedValue = myContext.getElementDefinition("id").newInstance(idPart); 1124 } 1125 1126 return massagedValue; 1127 } 1128 1129 private String toValue(IPrimitiveType<?> theOldPrimitive) { 1130 if (theOldPrimitive instanceof IIdType) { 1131 return ((IIdType) theOldPrimitive).getIdPart(); 1132 } 1133 return theOldPrimitive.getValueAsString(); 1134 } 1135}