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