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