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