
001/* 002 * #%L 003 * HAPI FHIR - Core Library 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.util; 021 022import ca.uhn.fhir.context.BaseRuntimeChildDefinition; 023import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; 024import ca.uhn.fhir.context.BaseRuntimeElementDefinition; 025import ca.uhn.fhir.context.BaseRuntimeElementDefinition.ChildTypeEnum; 026import ca.uhn.fhir.context.ConfigurationException; 027import ca.uhn.fhir.context.FhirContext; 028import ca.uhn.fhir.context.FhirVersionEnum; 029import ca.uhn.fhir.context.RuntimeChildChoiceDefinition; 030import ca.uhn.fhir.context.RuntimeChildDirectResource; 031import ca.uhn.fhir.context.RuntimeExtensionDtDefinition; 032import ca.uhn.fhir.context.RuntimeResourceDefinition; 033import ca.uhn.fhir.context.RuntimeSearchParam; 034import ca.uhn.fhir.i18n.Msg; 035import ca.uhn.fhir.interceptor.auth.CompartmentSearchParameterModifications; 036import ca.uhn.fhir.model.api.ExtensionDt; 037import ca.uhn.fhir.model.api.IIdentifiableElement; 038import ca.uhn.fhir.model.api.IResource; 039import ca.uhn.fhir.model.api.ISupportsUndeclaredExtensions; 040import ca.uhn.fhir.model.base.composite.BaseContainedDt; 041import ca.uhn.fhir.model.base.composite.BaseResourceReferenceDt; 042import ca.uhn.fhir.model.primitive.StringDt; 043import ca.uhn.fhir.parser.DataFormatException; 044import com.google.common.collect.Lists; 045import jakarta.annotation.Nonnull; 046import jakarta.annotation.Nullable; 047import org.apache.commons.lang3.StringUtils; 048import org.apache.commons.lang3.Validate; 049import org.hl7.fhir.instance.model.api.IBase; 050import org.hl7.fhir.instance.model.api.IBaseElement; 051import org.hl7.fhir.instance.model.api.IBaseExtension; 052import org.hl7.fhir.instance.model.api.IBaseHasExtensions; 053import org.hl7.fhir.instance.model.api.IBaseHasModifierExtensions; 054import org.hl7.fhir.instance.model.api.IBaseReference; 055import org.hl7.fhir.instance.model.api.IBaseResource; 056import org.hl7.fhir.instance.model.api.IDomainResource; 057import org.hl7.fhir.instance.model.api.IIdType; 058import org.hl7.fhir.instance.model.api.IPrimitiveType; 059import org.slf4j.Logger; 060import org.slf4j.LoggerFactory; 061 062import java.util.ArrayList; 063import java.util.Arrays; 064import java.util.Collection; 065import java.util.Collections; 066import java.util.Comparator; 067import java.util.HashMap; 068import java.util.HashSet; 069import java.util.IdentityHashMap; 070import java.util.Iterator; 071import java.util.List; 072import java.util.Map; 073import java.util.Objects; 074import java.util.Optional; 075import java.util.Set; 076import java.util.UUID; 077import java.util.regex.Matcher; 078import java.util.regex.Pattern; 079import java.util.stream.Collectors; 080import java.util.stream.Stream; 081 082import static org.apache.commons.lang3.StringUtils.defaultString; 083import static org.apache.commons.lang3.StringUtils.isBlank; 084import static org.apache.commons.lang3.StringUtils.isEmpty; 085import static org.apache.commons.lang3.StringUtils.isNotBlank; 086import static org.apache.commons.lang3.function.Consumers.nop; 087 088public class FhirTerser { 089 090 private static final Pattern COMPARTMENT_MATCHER_PATH = 091 Pattern.compile("([a-zA-Z.]+)\\.where\\(resolve\\(\\) is ([a-zA-Z]+)\\)"); 092 093 private static final String USER_DATA_KEY_CONTAIN_RESOURCES_COMPLETED = 094 FhirTerser.class.getName() + "_CONTAIN_RESOURCES_COMPLETED"; 095 096 private final FhirContext myContext; 097 private static final Logger ourLog = LoggerFactory.getLogger(FhirTerser.class); 098 099 /** 100 * This comparator sorts IBaseReferences, and places any that are missing an ID at the end. Those with an ID go to the front. 101 */ 102 private static final Comparator<IBaseReference> REFERENCES_WITH_IDS_FIRST = 103 Comparator.nullsLast(Comparator.comparing(ref -> { 104 if (ref.getResource() == null) return true; 105 if (ref.getResource().getIdElement() == null) return true; 106 if (ref.getResource().getIdElement().getValue() == null) return true; 107 return false; 108 })); 109 110 public FhirTerser(FhirContext theContext) { 111 super(); 112 myContext = theContext; 113 } 114 115 private List<String> addNameToList(List<String> theCurrentList, BaseRuntimeChildDefinition theChildDefinition) { 116 if (theChildDefinition == null) return null; 117 if (theCurrentList == null || theCurrentList.isEmpty()) 118 return new ArrayList<>(Collections.singletonList(theChildDefinition.getElementName())); 119 List<String> newList = new ArrayList<>(theCurrentList); 120 newList.add(theChildDefinition.getElementName()); 121 return newList; 122 } 123 124 private ExtensionDt createEmptyExtensionDt(IBaseExtension<?, ?> theBaseExtension, String theUrl) { 125 return createEmptyExtensionDt(theBaseExtension, false, theUrl); 126 } 127 128 @SuppressWarnings("unchecked") 129 private ExtensionDt createEmptyExtensionDt(IBaseExtension theBaseExtension, boolean theIsModifier, String theUrl) { 130 ExtensionDt retVal = new ExtensionDt(theIsModifier, theUrl); 131 theBaseExtension.getExtension().add(retVal); 132 return retVal; 133 } 134 135 private ExtensionDt createEmptyExtensionDt( 136 ISupportsUndeclaredExtensions theSupportsUndeclaredExtensions, String theUrl) { 137 return createEmptyExtensionDt(theSupportsUndeclaredExtensions, false, theUrl); 138 } 139 140 private ExtensionDt createEmptyExtensionDt( 141 ISupportsUndeclaredExtensions theSupportsUndeclaredExtensions, boolean theIsModifier, String theUrl) { 142 return theSupportsUndeclaredExtensions.addUndeclaredExtension(theIsModifier, theUrl); 143 } 144 145 private IBaseExtension<?, ?> createEmptyExtension(IBaseHasExtensions theBaseHasExtensions, String theUrl) { 146 return (IBaseExtension<?, ?>) theBaseHasExtensions.addExtension().setUrl(theUrl); 147 } 148 149 private IBaseExtension<?, ?> createEmptyModifierExtension( 150 IBaseHasModifierExtensions theBaseHasModifierExtensions, String theUrl) { 151 return (IBaseExtension<?, ?>) 152 theBaseHasModifierExtensions.addModifierExtension().setUrl(theUrl); 153 } 154 155 private ExtensionDt createEmptyModifierExtensionDt( 156 ISupportsUndeclaredExtensions theSupportsUndeclaredExtensions, String theUrl) { 157 return createEmptyExtensionDt(theSupportsUndeclaredExtensions, true, theUrl); 158 } 159 160 /** 161 * Clones all values from a source object into the equivalent fields in a target object 162 * 163 * @param theSource The source object (must not be null) 164 * @param theTarget The target object to copy values into (must not be null) 165 * @param theIgnoreMissingFields The ignore fields in the target which do not exist (if false, an exception will be thrown if the target is unable to accept a value from the source) 166 * @return Returns the target (which will be the same object that was passed into theTarget) for easy chaining 167 */ 168 public IBase cloneInto(IBase theSource, IBase theTarget, boolean theIgnoreMissingFields) { 169 Validate.notNull(theSource, "theSource must not be null"); 170 Validate.notNull(theTarget, "theTarget must not be null"); 171 172 // DSTU3+ 173 if (theSource instanceof IBaseElement) { 174 IBaseElement source = (IBaseElement) theSource; 175 IBaseElement target = (IBaseElement) theTarget; 176 target.setId(source.getId()); 177 } 178 179 // DSTU2 only 180 if (theSource instanceof IIdentifiableElement) { 181 IIdentifiableElement source = (IIdentifiableElement) theSource; 182 IIdentifiableElement target = (IIdentifiableElement) theTarget; 183 target.setElementSpecificId(source.getElementSpecificId()); 184 } 185 186 // DSTU2 only 187 if (theSource instanceof IResource) { 188 IResource source = (IResource) theSource; 189 IResource target = (IResource) theTarget; 190 target.setId(source.getId()); 191 target.getResourceMetadata().putAll(source.getResourceMetadata()); 192 } 193 194 if (theSource instanceof IPrimitiveType<?>) { 195 if (theTarget instanceof IPrimitiveType<?>) { 196 String valueAsString = ((IPrimitiveType<?>) theSource).getValueAsString(); 197 if (isNotBlank(valueAsString)) { 198 ((IPrimitiveType<?>) theTarget).setValueAsString(valueAsString); 199 } 200 if (theSource instanceof IBaseHasExtensions && theTarget instanceof IBaseHasExtensions) { 201 List<? extends IBaseExtension<?, ?>> extensions = ((IBaseHasExtensions) theSource).getExtension(); 202 for (IBaseExtension<?, ?> nextSource : extensions) { 203 IBaseExtension<?, ?> nextTarget = ((IBaseHasExtensions) theTarget).addExtension(); 204 cloneInto(nextSource, nextTarget, theIgnoreMissingFields); 205 } 206 } 207 return theSource; 208 } 209 if (theIgnoreMissingFields) { 210 return theSource; 211 } 212 throw new DataFormatException(Msg.code(1788) + "Can not copy value from primitive of type " 213 + theSource.getClass().getName() + " into type " 214 + theTarget.getClass().getName()); 215 } 216 217 if (theSource instanceof IBaseReference && theTarget instanceof IBaseReference) { 218 IBaseReference sourceReference = (IBaseReference) theSource; 219 IBaseReference targetReference = (IBaseReference) theTarget; 220 if (sourceReference.getResource() != null) { 221 targetReference.setResource(sourceReference.getResource()); 222 } 223 } 224 225 BaseRuntimeElementCompositeDefinition<?> sourceDef = 226 (BaseRuntimeElementCompositeDefinition<?>) myContext.getElementDefinition(theSource.getClass()); 227 BaseRuntimeElementCompositeDefinition<?> targetDef = 228 (BaseRuntimeElementCompositeDefinition<?>) myContext.getElementDefinition(theTarget.getClass()); 229 230 List<BaseRuntimeChildDefinition> children = sourceDef.getChildren(); 231 if (sourceDef instanceof RuntimeExtensionDtDefinition) { 232 children = ((RuntimeExtensionDtDefinition) sourceDef).getChildrenIncludingUrl(); 233 } 234 235 for (BaseRuntimeChildDefinition nextChild : children) 236 for (IBase nextValue : nextChild.getAccessor().getValues(theSource)) { 237 Class<? extends IBase> valueType = nextValue.getClass(); 238 String elementName = nextChild.getChildNameByDatatype(valueType); 239 BaseRuntimeChildDefinition targetChild = targetDef.getChildByName(elementName); 240 if (targetChild == null) { 241 if (theIgnoreMissingFields) { 242 continue; 243 } 244 throw new DataFormatException(Msg.code(1789) + "Type " 245 + theTarget.getClass().getName() + " does not have a child with name " + elementName); 246 } 247 248 BaseRuntimeElementDefinition<?> element = myContext.getElementDefinition(valueType); 249 Object instanceConstructorArg = targetChild.getInstanceConstructorArguments(); 250 IBase target; 251 if (element == null && BaseContainedDt.class.isAssignableFrom(valueType)) { 252 /* 253 * This is a hack for DSTU2 - The way we did contained resources in 254 * the DSTU2 model was weird, since the element isn't actually a FHIR type. 255 * This is fixed in DSTU3+ so this hack only applies there. 256 */ 257 BaseContainedDt containedTarget = (BaseContainedDt) ReflectionUtil.newInstance(valueType); 258 BaseContainedDt containedSource = (BaseContainedDt) nextValue; 259 for (IResource next : containedSource.getContainedResources()) { 260 List containedResources = containedTarget.getContainedResources(); 261 containedResources.add(next); 262 } 263 targetChild.getMutator().addValue(theTarget, containedTarget); 264 continue; 265 } else if (instanceConstructorArg != null) { 266 target = element.newInstance(instanceConstructorArg); 267 } else { 268 target = element.newInstance(); 269 } 270 271 targetChild.getMutator().addValue(theTarget, target); 272 cloneInto(nextValue, target, theIgnoreMissingFields); 273 } 274 275 return theTarget; 276 } 277 278 /** 279 * Returns a list containing all child elements (including the resource itself) which are <b>non-empty</b> and are either of the exact type specified, or are a subclass of that type. 280 * <p> 281 * For example, specifying a type of {@link StringDt} would return all non-empty string instances within the message. Specifying a type of {@link IResource} would return the resource itself, as 282 * well as any contained resources. 283 * </p> 284 * <p> 285 * Note on scope: This method will descend into any contained resources ({@link IResource#getContained()}) as well, but will not descend into linked resources (e.g. 286 * {@link BaseResourceReferenceDt#getResource()}) or embedded resources (e.g. Bundle.entry.resource) 287 * </p> 288 * 289 * @param theResource The resource instance to search. Must not be null. 290 * @param theType The type to search for. Must not be null. 291 * @return Returns a list of all matching elements 292 */ 293 public <T extends IBase> List<T> getAllPopulatedChildElementsOfType( 294 IBaseResource theResource, final Class<T> theType) { 295 final ArrayList<T> retVal = new ArrayList<>(); 296 BaseRuntimeElementCompositeDefinition<?> def = myContext.getResourceDefinition(theResource); 297 visit(newMap(), theResource, theResource, null, null, def, new IModelVisitor() { 298 @SuppressWarnings("unchecked") 299 @Override 300 public void acceptElement( 301 IBaseResource theOuterResource, 302 IBase theElement, 303 List<String> thePathToElement, 304 BaseRuntimeChildDefinition theChildDefinition, 305 BaseRuntimeElementDefinition<?> theDefinition) { 306 if (theElement == null || theElement.isEmpty()) { 307 return; 308 } 309 310 if (theType.isAssignableFrom(theElement.getClass())) { 311 retVal.add((T) theElement); 312 } 313 } 314 }); 315 return retVal; 316 } 317 318 /** 319 * Extracts all outbound references from a resource 320 * 321 * @param theResource the resource to be analyzed 322 * @return a list of references to other resources 323 */ 324 public List<ResourceReferenceInfo> getAllResourceReferences(final IBaseResource theResource) { 325 return getAllResourceReferencesExcluding(theResource, Lists.newArrayList()); 326 } 327 328 /** 329 * Extracts all outbound references from a resource, excluding any that are located on black-listed parts of the 330 * resource 331 * 332 * @param theResource the resource to be analyzed 333 * @param thePathsToExclude a list of dot-delimited paths not to include in the result 334 * @return a list of references to other resources 335 */ 336 public List<ResourceReferenceInfo> getAllResourceReferencesExcluding( 337 final IBaseResource theResource, List<String> thePathsToExclude) { 338 final ArrayList<ResourceReferenceInfo> retVal = new ArrayList<>(); 339 BaseRuntimeElementCompositeDefinition<?> def = myContext.getResourceDefinition(theResource); 340 List<List<String>> tokenizedPathsToExclude = thePathsToExclude.stream() 341 .map(path -> StringUtils.split(path, ".")) 342 .map(Lists::newArrayList) 343 .collect(Collectors.toList()); 344 345 visit(newMap(), theResource, theResource, null, null, def, new IModelVisitor() { 346 @Override 347 public void acceptElement( 348 IBaseResource theOuterResource, 349 IBase theElement, 350 List<String> thePathToElement, 351 BaseRuntimeChildDefinition theChildDefinition, 352 BaseRuntimeElementDefinition<?> theDefinition) { 353 if (theElement == null || theElement.isEmpty()) { 354 return; 355 } 356 357 if (thePathToElement != null && pathShouldBeExcluded(tokenizedPathsToExclude, thePathToElement)) { 358 return; 359 } 360 if (IBaseReference.class.isAssignableFrom(theElement.getClass())) { 361 retVal.add(new ResourceReferenceInfo( 362 myContext, theOuterResource, thePathToElement, (IBaseReference) theElement)); 363 } 364 } 365 }); 366 return retVal; 367 } 368 369 private boolean pathShouldBeExcluded(List<List<String>> theTokenizedPathsToExclude, List<String> thePathToElement) { 370 return theTokenizedPathsToExclude.stream().anyMatch(p -> { 371 // Check whether the path to the element starts with the path to be excluded 372 if (p.size() > thePathToElement.size()) { 373 return false; 374 } 375 376 List<String> prefix = thePathToElement.subList(0, p.size()); 377 378 return Objects.equals(p, prefix); 379 }); 380 } 381 382 private BaseRuntimeChildDefinition getDefinition( 383 BaseRuntimeElementCompositeDefinition<?> theCurrentDef, List<String> theSubList) { 384 BaseRuntimeChildDefinition nextDef = theCurrentDef.getChildByNameOrThrowDataFormatException(theSubList.get(0)); 385 386 if (theSubList.size() == 1) { 387 return nextDef; 388 } 389 BaseRuntimeElementCompositeDefinition<?> cmp = 390 (BaseRuntimeElementCompositeDefinition<?>) nextDef.getChildByName(theSubList.get(0)); 391 return getDefinition(cmp, theSubList.subList(1, theSubList.size())); 392 } 393 394 public BaseRuntimeChildDefinition getDefinition(Class<? extends IBaseResource> theResourceType, String thePath) { 395 RuntimeResourceDefinition def = myContext.getResourceDefinition(theResourceType); 396 397 List<String> parts = Arrays.asList(thePath.split("\\.")); 398 List<String> subList = parts.subList(1, parts.size()); 399 if (subList.size() < 1) { 400 throw new ConfigurationException(Msg.code(1790) + "Invalid path: " + thePath); 401 } 402 return getDefinition(def, subList); 403 } 404 405 public Object getSingleValueOrNull(IBase theTarget, String thePath) { 406 Class<IBase> wantedType = IBase.class; 407 408 return getSingleValueOrNull(theTarget, thePath, wantedType); 409 } 410 411 public <T extends IBase> T getSingleValueOrNull(IBase theTarget, String thePath, Class<T> theWantedType) { 412 Validate.notNull(theTarget, "theTarget must not be null"); 413 Validate.notBlank(thePath, "thePath must not be empty"); 414 415 BaseRuntimeElementDefinition<?> def = myContext.getElementDefinition(theTarget.getClass()); 416 if (!(def instanceof BaseRuntimeElementCompositeDefinition)) { 417 throw new IllegalArgumentException(Msg.code(1791) + "Target is not a composite type: " 418 + theTarget.getClass().getName()); 419 } 420 421 BaseRuntimeElementCompositeDefinition<?> currentDef = (BaseRuntimeElementCompositeDefinition<?>) def; 422 423 List<String> parts = parsePath(currentDef, thePath); 424 425 List<T> retVal = getValues(currentDef, theTarget, parts, theWantedType); 426 if (retVal.isEmpty()) { 427 return null; 428 } 429 return retVal.get(0); 430 } 431 432 public Optional<String> getSinglePrimitiveValue(IBase theTarget, String thePath) { 433 return getSingleValue(theTarget, thePath, IPrimitiveType.class).map(t -> t.getValueAsString()); 434 } 435 436 public String getSinglePrimitiveValueOrNull(IBase theTarget, String thePath) { 437 return getSingleValue(theTarget, thePath, IPrimitiveType.class) 438 .map(IPrimitiveType::getValueAsString) 439 .orElse(null); 440 } 441 442 public <T extends IBase> Optional<T> getSingleValue(IBase theTarget, String thePath, Class<T> theWantedType) { 443 return Optional.ofNullable(getSingleValueOrNull(theTarget, thePath, theWantedType)); 444 } 445 446 private <T extends IBase> List<T> getValues( 447 BaseRuntimeElementCompositeDefinition<?> theCurrentDef, 448 IBase theCurrentObj, 449 List<String> theSubList, 450 Class<T> theWantedClass) { 451 return getValues(theCurrentDef, theCurrentObj, theSubList, theWantedClass, false, false); 452 } 453 454 @SuppressWarnings("unchecked") 455 private <T extends IBase> List<T> getValues( 456 BaseRuntimeElementCompositeDefinition<?> theCurrentDef, 457 IBase theCurrentObj, 458 List<String> theSubList, 459 Class<T> theWantedClass, 460 boolean theCreate, 461 boolean theAddExtension) { 462 if (theSubList.isEmpty()) { 463 return Collections.emptyList(); 464 } 465 466 String name = theSubList.get(0); 467 List<T> retVal = new ArrayList<>(); 468 469 if (name.startsWith("extension('")) { 470 String extensionUrl = name.substring("extension('".length()); 471 int endIndex = extensionUrl.indexOf('\''); 472 if (endIndex != -1) { 473 extensionUrl = extensionUrl.substring(0, endIndex); 474 } 475 476 if (myContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) { 477 // DTSU2 478 final String extensionDtUrlForLambda = extensionUrl; 479 List<ExtensionDt> extensionDts = Collections.emptyList(); 480 if (theCurrentObj instanceof ISupportsUndeclaredExtensions) { 481 extensionDts = ((ISupportsUndeclaredExtensions) theCurrentObj) 482 .getUndeclaredExtensions().stream() 483 .filter(t -> t.getUrl().equals(extensionDtUrlForLambda)) 484 .collect(Collectors.toList()); 485 486 if (theAddExtension 487 && (!(theCurrentObj instanceof IBaseExtension) 488 || (extensionDts.isEmpty() && theSubList.size() == 1))) { 489 extensionDts.add( 490 createEmptyExtensionDt((ISupportsUndeclaredExtensions) theCurrentObj, extensionUrl)); 491 } 492 493 if (extensionDts.isEmpty() && theCreate) { 494 extensionDts.add( 495 createEmptyExtensionDt((ISupportsUndeclaredExtensions) theCurrentObj, extensionUrl)); 496 } 497 498 } else if (theCurrentObj instanceof IBaseExtension) { 499 extensionDts = ((IBaseExtension) theCurrentObj).getExtension(); 500 501 if (theAddExtension && (extensionDts.isEmpty() && theSubList.size() == 1)) { 502 extensionDts.add(createEmptyExtensionDt((IBaseExtension) theCurrentObj, extensionUrl)); 503 } 504 505 if (extensionDts.isEmpty() && theCreate) { 506 extensionDts.add(createEmptyExtensionDt((IBaseExtension) theCurrentObj, extensionUrl)); 507 } 508 } 509 510 for (ExtensionDt next : extensionDts) { 511 if (theWantedClass.isAssignableFrom(next.getClass())) { 512 retVal.add((T) next); 513 } 514 } 515 } else { 516 // DSTU3+ 517 final String extensionUrlForLambda = extensionUrl; 518 List<IBaseExtension<?, ?>> extensions = Collections.emptyList(); 519 if (theCurrentObj instanceof IBaseHasExtensions) { 520 extensions = ((IBaseHasExtensions) theCurrentObj) 521 .getExtension().stream() 522 .filter(t -> t.getUrl().equals(extensionUrlForLambda)) 523 .collect(Collectors.toList()); 524 525 if (theAddExtension 526 && (!(theCurrentObj instanceof IBaseExtension) 527 || (extensions.isEmpty() && theSubList.size() == 1))) { 528 extensions.add(createEmptyExtension((IBaseHasExtensions) theCurrentObj, extensionUrl)); 529 } 530 531 if (extensions.isEmpty() && theCreate) { 532 extensions.add(createEmptyExtension((IBaseHasExtensions) theCurrentObj, extensionUrl)); 533 } 534 } 535 536 for (IBaseExtension<?, ?> next : extensions) { 537 if (theWantedClass.isAssignableFrom(next.getClass())) { 538 retVal.add((T) next); 539 } 540 } 541 } 542 543 if (theSubList.size() > 1) { 544 List<T> values = retVal; 545 retVal = new ArrayList<>(); 546 for (T nextElement : values) { 547 BaseRuntimeElementCompositeDefinition<?> nextChildDef = (BaseRuntimeElementCompositeDefinition<?>) 548 myContext.getElementDefinition(nextElement.getClass()); 549 List<T> foundValues = getValues( 550 nextChildDef, 551 nextElement, 552 theSubList.subList(1, theSubList.size()), 553 theWantedClass, 554 theCreate, 555 theAddExtension); 556 retVal.addAll(foundValues); 557 } 558 } 559 560 return retVal; 561 } 562 563 if (name.startsWith("modifierExtension('")) { 564 String extensionUrl = name.substring("modifierExtension('".length()); 565 int endIndex = extensionUrl.indexOf('\''); 566 if (endIndex != -1) { 567 extensionUrl = extensionUrl.substring(0, endIndex); 568 } 569 570 if (myContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) { 571 // DSTU2 572 final String extensionDtUrlForLambda = extensionUrl; 573 List<ExtensionDt> extensionDts = Collections.emptyList(); 574 if (theCurrentObj instanceof ISupportsUndeclaredExtensions) { 575 extensionDts = ((ISupportsUndeclaredExtensions) theCurrentObj) 576 .getUndeclaredModifierExtensions().stream() 577 .filter(t -> t.getUrl().equals(extensionDtUrlForLambda)) 578 .collect(Collectors.toList()); 579 580 if (theAddExtension 581 && (!(theCurrentObj instanceof IBaseExtension) 582 || (extensionDts.isEmpty() && theSubList.size() == 1))) { 583 extensionDts.add(createEmptyModifierExtensionDt( 584 (ISupportsUndeclaredExtensions) theCurrentObj, extensionUrl)); 585 } 586 587 if (extensionDts.isEmpty() && theCreate) { 588 extensionDts.add(createEmptyModifierExtensionDt( 589 (ISupportsUndeclaredExtensions) theCurrentObj, extensionUrl)); 590 } 591 592 } else if (theCurrentObj instanceof IBaseExtension) { 593 extensionDts = ((IBaseExtension) theCurrentObj).getExtension(); 594 595 if (theAddExtension && (extensionDts.isEmpty() && theSubList.size() == 1)) { 596 extensionDts.add(createEmptyExtensionDt((IBaseExtension) theCurrentObj, extensionUrl)); 597 } 598 599 if (extensionDts.isEmpty() && theCreate) { 600 extensionDts.add(createEmptyExtensionDt((IBaseExtension) theCurrentObj, extensionUrl)); 601 } 602 } 603 604 for (ExtensionDt next : extensionDts) { 605 if (theWantedClass.isAssignableFrom(next.getClass())) { 606 retVal.add((T) next); 607 } 608 } 609 } else { 610 // DSTU3+ 611 final String extensionUrlForLambda = extensionUrl; 612 List<IBaseExtension<?, ?>> extensions = Collections.emptyList(); 613 614 if (theCurrentObj instanceof IBaseHasModifierExtensions) { 615 extensions = ((IBaseHasModifierExtensions) theCurrentObj) 616 .getModifierExtension().stream() 617 .filter(t -> t.getUrl().equals(extensionUrlForLambda)) 618 .collect(Collectors.toList()); 619 620 if (theAddExtension 621 && (!(theCurrentObj instanceof IBaseExtension) 622 || (extensions.isEmpty() && theSubList.size() == 1))) { 623 extensions.add( 624 createEmptyModifierExtension((IBaseHasModifierExtensions) theCurrentObj, extensionUrl)); 625 } 626 627 if (extensions.isEmpty() && theCreate) { 628 extensions.add( 629 createEmptyModifierExtension((IBaseHasModifierExtensions) theCurrentObj, extensionUrl)); 630 } 631 } 632 633 for (IBaseExtension<?, ?> next : extensions) { 634 if (theWantedClass.isAssignableFrom(next.getClass())) { 635 retVal.add((T) next); 636 } 637 } 638 } 639 640 if (theSubList.size() > 1) { 641 List<T> values = retVal; 642 retVal = new ArrayList<>(); 643 for (T nextElement : values) { 644 BaseRuntimeElementCompositeDefinition<?> nextChildDef = (BaseRuntimeElementCompositeDefinition<?>) 645 myContext.getElementDefinition(nextElement.getClass()); 646 List<T> foundValues = getValues( 647 nextChildDef, 648 nextElement, 649 theSubList.subList(1, theSubList.size()), 650 theWantedClass, 651 theCreate, 652 theAddExtension); 653 retVal.addAll(foundValues); 654 } 655 } 656 657 return retVal; 658 } 659 660 BaseRuntimeChildDefinition nextDef = theCurrentDef.getChildByNameOrThrowDataFormatException(name); 661 List<? extends IBase> values = nextDef.getAccessor().getValues(theCurrentObj); 662 663 if (values.isEmpty() && theCreate) { 664 BaseRuntimeElementDefinition<?> childByName = nextDef.getChildByName(name); 665 Object arg = nextDef.getInstanceConstructorArguments(); 666 IBase value; 667 if (arg != null) { 668 value = childByName.newInstance(arg); 669 } else { 670 value = childByName.newInstance(); 671 } 672 nextDef.getMutator().addValue(theCurrentObj, value); 673 List<IBase> list = new ArrayList<>(); 674 list.add(value); 675 values = list; 676 } 677 678 if (theSubList.size() == 1) { 679 if (nextDef instanceof RuntimeChildChoiceDefinition) { 680 for (IBase next : values) { 681 if (next != null) { 682 if (name.endsWith("[x]")) { 683 if (theWantedClass == null || theWantedClass.isAssignableFrom(next.getClass())) { 684 retVal.add((T) next); 685 } 686 } else { 687 String childName = nextDef.getChildNameByDatatype(next.getClass()); 688 if (theSubList.get(0).equals(childName)) { 689 if (theWantedClass == null || theWantedClass.isAssignableFrom(next.getClass())) { 690 retVal.add((T) next); 691 } 692 } 693 } 694 } 695 } 696 } else { 697 for (IBase next : values) { 698 if (next != null) { 699 if (theWantedClass == null || theWantedClass.isAssignableFrom(next.getClass())) { 700 retVal.add((T) next); 701 } 702 } 703 } 704 } 705 } else { 706 for (IBase nextElement : values) { 707 // We can only continue iterating if the current element is composite. 708 // If we haven't reached the end of the path, and we have already reached an element 709 // with a primitive data type, we did not find a match. 710 if (myContext.getElementDefinition(nextElement.getClass()) 711 instanceof BaseRuntimeElementCompositeDefinition<?> nextChildDef) { 712 List<T> foundValues = getValues( 713 nextChildDef, 714 nextElement, 715 theSubList.subList(1, theSubList.size()), 716 theWantedClass, 717 theCreate, 718 theAddExtension); 719 retVal.addAll(foundValues); 720 } 721 } 722 } 723 return retVal; 724 } 725 726 /** 727 * Returns values stored in an element identified by its path. The list of values is of 728 * type {@link Object}. 729 * 730 * @param theElement The element to be accessed. Must not be null. 731 * @param thePath The path for the element to be accessed.@param theElement The resource instance to be accessed. Must not be null. 732 * @return A list of values of type {@link Object}. 733 */ 734 public List<IBase> getValues(IBase theElement, String thePath) { 735 Class<IBase> wantedClass = IBase.class; 736 737 return getValues(theElement, thePath, wantedClass); 738 } 739 740 /** 741 * Returns values stored in an element identified by its path. The list of values is of 742 * type {@link Object}. 743 * 744 * @param theElement The element to be accessed. Must not be null. 745 * @param thePath The path for the element to be accessed. 746 * @param theCreate When set to <code>true</code>, the terser will create a null-valued element where none exists. 747 * @return A list of values of type {@link Object}. 748 */ 749 public List<IBase> getValues(IBase theElement, String thePath, boolean theCreate) { 750 Class<IBase> wantedClass = IBase.class; 751 752 return getValues(theElement, thePath, wantedClass, theCreate); 753 } 754 755 /** 756 * Returns values stored in an element identified by its path. The list of values is of 757 * type {@link Object}. 758 * 759 * @param theElement The element to be accessed. Must not be null. 760 * @param thePath The path for the element to be accessed. 761 * @param theCreate When set to <code>true</code>, the terser will create a null-valued element where none exists. 762 * @param theAddExtension When set to <code>true</code>, the terser will add a null-valued extension where one or more such extensions already exist. 763 * @return A list of values of type {@link Object}. 764 */ 765 public List<IBase> getValues(IBase theElement, String thePath, boolean theCreate, boolean theAddExtension) { 766 Class<IBase> wantedClass = IBase.class; 767 768 return getValues(theElement, thePath, wantedClass, theCreate, theAddExtension); 769 } 770 771 /** 772 * Returns values stored in an element identified by its path. The list of values is of 773 * type <code>theWantedClass</code>. 774 * 775 * @param theElement The element to be accessed. Must not be null. 776 * @param thePath The path for the element to be accessed. 777 * @param theWantedClass The desired class to be returned in a list. 778 * @param <T> Type declared by <code>theWantedClass</code> 779 * @return A list of values of type <code>theWantedClass</code>. 780 */ 781 public <T extends IBase> List<T> getValues(IBase theElement, String thePath, Class<T> theWantedClass) { 782 BaseRuntimeElementCompositeDefinition<?> def = 783 (BaseRuntimeElementCompositeDefinition<?>) myContext.getElementDefinition(theElement.getClass()); 784 List<String> parts = parsePath(def, thePath); 785 return getValues(def, theElement, parts, theWantedClass); 786 } 787 788 /** 789 * Returns values stored in an element identified by its path. The list of values is of 790 * type <code>theWantedClass</code>. 791 * 792 * @param theElement The element to be accessed. Must not be null. 793 * @param thePath The path for the element to be accessed. 794 * @param theWantedClass The desired class to be returned in a list. 795 * @param theCreate When set to <code>true</code>, the terser will create a null-valued element where none exists. 796 * @param <T> Type declared by <code>theWantedClass</code> 797 * @return A list of values of type <code>theWantedClass</code>. 798 */ 799 public <T extends IBase> List<T> getValues( 800 IBase theElement, String thePath, Class<T> theWantedClass, boolean theCreate) { 801 BaseRuntimeElementCompositeDefinition<?> def = 802 (BaseRuntimeElementCompositeDefinition<?>) myContext.getElementDefinition(theElement.getClass()); 803 List<String> parts = parsePath(def, thePath); 804 return getValues(def, theElement, parts, theWantedClass, theCreate, false); 805 } 806 807 /** 808 * Returns values stored in an element identified by its path. The list of values is of 809 * type <code>theWantedClass</code>. 810 * 811 * @param theElement The element to be accessed. Must not be null. 812 * @param thePath The path for the element to be accessed. 813 * @param theWantedClass The desired class to be returned in a list. 814 * @param theCreate When set to <code>true</code>, the terser will create a null-valued element where none exists. 815 * @param theAddExtension When set to <code>true</code>, the terser will add a null-valued extension where one or more such extensions already exist. 816 * @param <T> Type declared by <code>theWantedClass</code> 817 * @return A list of values of type <code>theWantedClass</code>. 818 */ 819 public <T extends IBase> List<T> getValues( 820 IBase theElement, String thePath, Class<T> theWantedClass, boolean theCreate, boolean theAddExtension) { 821 BaseRuntimeElementCompositeDefinition<?> def = 822 (BaseRuntimeElementCompositeDefinition<?>) myContext.getElementDefinition(theElement.getClass()); 823 List<String> parts = parsePath(def, thePath); 824 return getValues(def, theElement, parts, theWantedClass, theCreate, theAddExtension); 825 } 826 827 private List<String> parsePath(BaseRuntimeElementCompositeDefinition<?> theElementDef, String thePath) { 828 List<String> parts = new ArrayList<>(); 829 830 int currentStart = 0; 831 boolean inSingleQuote = false; 832 for (int i = 0; i < thePath.length(); i++) { 833 switch (thePath.charAt(i)) { 834 case '\'': 835 inSingleQuote = !inSingleQuote; 836 break; 837 case '.': 838 if (!inSingleQuote) { 839 parts.add(thePath.substring(currentStart, i)); 840 currentStart = i + 1; 841 } 842 break; 843 } 844 } 845 846 parts.add(thePath.substring(currentStart)); 847 848 String firstPart = parts.get(0); 849 if (Character.isUpperCase(firstPart.charAt(0)) && theElementDef instanceof RuntimeResourceDefinition) { 850 if (firstPart.equals(theElementDef.getName())) { 851 parts = parts.subList(1, parts.size()); 852 } else { 853 parts = Collections.emptyList(); 854 return parts; 855 } 856 } else if (firstPart.equals(theElementDef.getName())) { 857 parts = parts.subList(1, parts.size()); 858 } 859 860 if (parts.size() < 1) { 861 throw new ConfigurationException(Msg.code(1792) + "Invalid path: " + thePath); 862 } 863 return parts; 864 } 865 866 /** 867 * Returns <code>true</code> if <code>theSource</code> is in the compartment named <code>theCompartmentName</code> 868 * belonging to resource <code>theTarget</code> 869 * 870 * @param theCompartmentName The name of the compartment 871 * @param theSource The potential member of the compartment 872 * @param theTarget The owner of the compartment. Note that both the resource type and ID must be filled in on this IIdType or the method will throw an {@link IllegalArgumentException} 873 * @return <code>true</code> if <code>theSource</code> is in the compartment 874 * @throws IllegalArgumentException If theTarget does not contain both a resource type and ID 875 */ 876 public boolean isSourceInCompartmentForTarget( 877 String theCompartmentName, IBaseResource theSource, IIdType theTarget) { 878 return isSourceInCompartmentForTarget( 879 theCompartmentName, theSource, theTarget, new CompartmentSearchParameterModifications()); 880 } 881 882 @Deprecated 883 public boolean isSourceInCompartmentForTarget( 884 String theCompartmentName, 885 IBaseResource theSource, 886 IIdType theTarget, 887 @Nullable Set<String> theAdditionalCompartmentParamNames) { 888 return isSourceInCompartmentForTarget( 889 theCompartmentName, 890 theSource, 891 theTarget, 892 CompartmentSearchParameterModifications.fromAdditionalCompartmentParamNames( 893 myContext.getResourceType(theSource), 894 theAdditionalCompartmentParamNames == null ? Set.of() : theAdditionalCompartmentParamNames)); 895 } 896 897 /** 898 * Returns <code>true</code> if <code>theSource</code> is in the compartment named <code>theCompartmentName</code> 899 * belonging to resource <code>theTarget</code> 900 * 901 * @param theCompartmentName The name of the compartment 902 * @param theSource The potential member of the compartment 903 * @param theTarget The owner of the compartment. Note that both the resource type and ID must be filled in on this IIdType or the method will throw an {@link IllegalArgumentException} 904 * @param theModifications If provided, SP modifications to the compartment 905 * @return <code>true</code> if <code>theSource</code> is in the compartment or one of the additional parameters matched. 906 * @throws IllegalArgumentException If theTarget does not contain both a resource type and ID 907 */ 908 public boolean isSourceInCompartmentForTarget( 909 String theCompartmentName, 910 IBaseResource theSource, 911 IIdType theTarget, 912 CompartmentSearchParameterModifications theModifications) { 913 Validate.notBlank(theCompartmentName, "theCompartmentName must not be null or blank"); 914 Validate.notNull(theSource, "theSource must not be null"); 915 Validate.notNull(theTarget, "theTarget must not be null"); 916 Validate.notBlank( 917 defaultString(theTarget.getResourceType()), 918 "theTarget must have a populated resource type (theTarget.getResourceType() does not return a value)"); 919 Validate.notBlank( 920 defaultString(theTarget.getIdPart()), 921 "theTarget must have a populated ID (theTarget.getIdPart() does not return a value)"); 922 923 String wantRef = theTarget.toUnqualifiedVersionless().getValue(); 924 925 RuntimeResourceDefinition sourceDef = myContext.getResourceDefinition(theSource); 926 if (theSource.getIdElement().hasIdPart()) { 927 if (wantRef.equals( 928 sourceDef.getName() + '/' + theSource.getIdElement().getIdPart())) { 929 return true; 930 } 931 } 932 933 class CompartmentOwnerVisitor implements ICompartmentOwnerVisitor { 934 935 private final String myWantRef; 936 937 public boolean isFound() { 938 return myFound; 939 } 940 941 private boolean myFound; 942 943 public CompartmentOwnerVisitor(String theWantRef) { 944 myWantRef = theWantRef; 945 } 946 947 @Override 948 public boolean consume(IIdType theCompartmentOwner) { 949 if (myWantRef.equals( 950 theCompartmentOwner.toUnqualifiedVersionless().getValue())) { 951 myFound = true; 952 } 953 return !myFound; 954 } 955 } 956 957 CompartmentOwnerVisitor consumer = new CompartmentOwnerVisitor(wantRef); 958 visitCompartmentOwnersForResource(theCompartmentName, theSource, theModifications, consumer); 959 return consumer.isFound(); 960 } 961 962 public List<IIdType> getCompartmentOwnersForResource( 963 String theCompartmentName, 964 IBaseResource theSource, 965 @Nullable Set<String> theAdditionalCompartmentParamNames) { 966 return getCompartmentOwnersForResource( 967 theCompartmentName, 968 theSource, 969 CompartmentSearchParameterModifications.fromAdditionalCompartmentParamNames( 970 myContext.getResourceType(theCompartmentName), 971 theAdditionalCompartmentParamNames == null ? Set.of() : theAdditionalCompartmentParamNames)); 972 } 973 974 /** 975 * Returns the owners of the compartment in <code>theSource</code> is in the compartment named <code>theCompartmentName</code>. 976 * 977 * @param theCompartmentName The name of the compartment 978 * @param theSource The potential member of the compartment 979 * @param theModifications If provided, defines compartment modifications to be considered 980 */ 981 @Nonnull 982 public List<IIdType> getCompartmentOwnersForResource( 983 String theCompartmentName, 984 IBaseResource theSource, 985 CompartmentSearchParameterModifications theModifications) { 986 Validate.notBlank(theCompartmentName, "theCompartmentName must not be null or blank"); 987 Validate.notNull(theSource, "theSource must not be null"); 988 989 class CompartmentOwnerVisitor implements ICompartmentOwnerVisitor { 990 991 private final Set<String> myOwnersAdded = new HashSet<>(); 992 private final List<IIdType> myOwners = new ArrayList<>(2); 993 994 public List<IIdType> getOwners() { 995 return myOwners; 996 } 997 998 @Override 999 public boolean consume(IIdType theCompartmentOwner) { 1000 if (myOwnersAdded.add(theCompartmentOwner.getValue())) { 1001 myOwners.add(theCompartmentOwner); 1002 } 1003 return true; 1004 } 1005 } 1006 1007 CompartmentOwnerVisitor consumer = new CompartmentOwnerVisitor(); 1008 visitCompartmentOwnersForResource(theCompartmentName, theSource, theModifications, consumer); 1009 return consumer.getOwners(); 1010 } 1011 1012 public Stream<IBaseReference> getCompartmentReferencesForResource( 1013 String theCompartmentName, 1014 IBaseResource theSource, 1015 @Nullable Set<String> theAdditionalCompartmentParamNames) { 1016 return getCompartmentReferencesForResource( 1017 theCompartmentName, 1018 theSource, 1019 CompartmentSearchParameterModifications.fromAdditionalCompartmentParamNames( 1020 myContext.getResourceType(theCompartmentName), 1021 theAdditionalCompartmentParamNames == null ? Set.of() : theAdditionalCompartmentParamNames)); 1022 } 1023 1024 @Nonnull 1025 public Stream<IBaseReference> getCompartmentReferencesForResource( 1026 String theCompartmentName, 1027 IBaseResource theSource, 1028 @Nullable CompartmentSearchParameterModifications theModifications) { 1029 Validate.notBlank(theCompartmentName, "theCompartmentName must not be null or blank"); 1030 Validate.notNull(theSource, "theSource must not be null"); 1031 Set<String> additionalSPNames; 1032 Set<String> omittedSPNames; 1033 if (theModifications != null) { 1034 String resourceType = myContext.getResourceType(theSource); 1035 additionalSPNames = theModifications.getAdditionalSearchParamNamesForResourceType(resourceType); 1036 omittedSPNames = theModifications.getOmittedSPNamesForResourceType(resourceType); 1037 } else { 1038 additionalSPNames = new HashSet<>(); 1039 omittedSPNames = new HashSet<>(); 1040 } 1041 1042 RuntimeResourceDefinition sourceDef = myContext.getResourceDefinition(theSource); 1043 List<RuntimeSearchParam> params = sourceDef.getSearchParamsForCompartmentName(theCompartmentName).stream() 1044 .filter(p -> !omittedSPNames.contains(p.getName())) 1045 .collect(Collectors.toList()); 1046 1047 additionalSPNames.stream() 1048 .map(sourceDef::getSearchParam) 1049 .filter(Objects::nonNull) 1050 .forEach(params::add); 1051 1052 return params.stream() 1053 .flatMap(nextParam -> nextParam.getPathsSplit().stream()) 1054 .filter(StringUtils::isNotBlank) 1055 .flatMap(nextPath -> { 1056 1057 /* 1058 * DSTU3 and before just defined compartments as being (e.g.) named 1059 * Patient with a path like CarePlan.subject 1060 * 1061 * R4 uses a fancier format like CarePlan.subject.where(resolve() is Patient) 1062 * 1063 * The following Regex is a hack to make that efficient at runtime. 1064 */ 1065 String wantType; 1066 Matcher matcher = COMPARTMENT_MATCHER_PATH.matcher(nextPath); 1067 if (matcher.matches()) { 1068 nextPath = matcher.group(1); 1069 wantType = matcher.group(2); 1070 } else { 1071 wantType = null; 1072 } 1073 1074 return getValues(theSource, nextPath, IBaseReference.class).stream() 1075 .filter(nextValue -> isEmpty(wantType) || wantType.equals(getTypeFromReference(nextValue))); 1076 }); 1077 } 1078 1079 private String getTypeFromReference(IBaseReference theReference) { 1080 1081 IIdType nextTargetId = theReference.getReferenceElement().toUnqualifiedVersionless(); 1082 1083 if (isNotBlank(nextTargetId.getResourceType())) { 1084 return nextTargetId.getResourceType(); 1085 } else if (theReference.getResource() != null) { 1086 /* 1087 * If the reference isn't an explicit resource ID, but instead is just 1088 * a resource object, we'll use that type. 1089 */ 1090 return myContext.getResourceType(theReference.getResource()); 1091 } else { 1092 return "No type on reference"; 1093 } 1094 } 1095 1096 private void visitCompartmentOwnersForResource( 1097 String theCompartmentName, 1098 IBaseResource theSource, 1099 CompartmentSearchParameterModifications theCompartmentModifications, 1100 ICompartmentOwnerVisitor theConsumer) { 1101 1102 getCompartmentReferencesForResource(theCompartmentName, theSource, theCompartmentModifications) 1103 .flatMap(nextValue -> { 1104 IIdType nextTargetId = nextValue.getReferenceElement().toUnqualifiedVersionless(); 1105 1106 /* 1107 * If the reference isn't an explicit resource ID, but instead is just 1108 * a resource object, we'll calculate its ID and treat the target 1109 * as that. 1110 */ 1111 if (isBlank(nextTargetId.getValue()) && nextValue.getResource() != null) { 1112 IBaseResource nextTarget = nextValue.getResource(); 1113 nextTargetId = nextTarget.getIdElement().toUnqualifiedVersionless(); 1114 if (!nextTargetId.hasResourceType()) { 1115 String resourceType = myContext.getResourceType(nextTarget); 1116 nextTargetId.setParts(null, resourceType, nextTargetId.getIdPart(), null); 1117 } 1118 } 1119 if (isNotBlank(nextTargetId.getValue())) { 1120 return Stream.of(nextTargetId); 1121 } else { 1122 return Stream.empty(); 1123 } 1124 }) 1125 .takeWhile(theConsumer::consume) 1126 .forEach(nop()); 1127 } 1128 1129 private void visit( 1130 IBase theElement, 1131 BaseRuntimeChildDefinition theChildDefinition, 1132 BaseRuntimeElementDefinition<?> theDefinition, 1133 IModelVisitor2 theCallback, 1134 List<IBase> theContainingElementPath, 1135 List<BaseRuntimeChildDefinition> theChildDefinitionPath, 1136 List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) { 1137 if (theChildDefinition != null) { 1138 theChildDefinitionPath.add(theChildDefinition); 1139 } 1140 theContainingElementPath.add(theElement); 1141 theElementDefinitionPath.add(theDefinition); 1142 1143 boolean recurse = theCallback.acceptElement( 1144 theElement, 1145 Collections.unmodifiableList(theContainingElementPath), 1146 Collections.unmodifiableList(theChildDefinitionPath), 1147 Collections.unmodifiableList(theElementDefinitionPath)); 1148 if (recurse) { 1149 1150 /* 1151 * Visit undeclared extensions 1152 */ 1153 if (theElement instanceof ISupportsUndeclaredExtensions) { 1154 ISupportsUndeclaredExtensions containingElement = (ISupportsUndeclaredExtensions) theElement; 1155 for (ExtensionDt nextExt : containingElement.getUndeclaredExtensions()) { 1156 theContainingElementPath.add(nextExt); 1157 theCallback.acceptUndeclaredExtension( 1158 nextExt, theContainingElementPath, theChildDefinitionPath, theElementDefinitionPath); 1159 theContainingElementPath.remove(theContainingElementPath.size() - 1); 1160 } 1161 } 1162 1163 /* 1164 * Now visit the children of the given element 1165 */ 1166 switch (theDefinition.getChildType()) { 1167 case ID_DATATYPE: 1168 case PRIMITIVE_XHTML_HL7ORG: 1169 case PRIMITIVE_XHTML: 1170 case PRIMITIVE_DATATYPE: 1171 // These are primitive types, so we don't need to visit their children 1172 break; 1173 case RESOURCE: 1174 case RESOURCE_BLOCK: 1175 case COMPOSITE_DATATYPE: { 1176 BaseRuntimeElementCompositeDefinition<?> childDef = 1177 (BaseRuntimeElementCompositeDefinition<?>) theDefinition; 1178 for (BaseRuntimeChildDefinition nextChild : childDef.getChildrenAndExtension()) { 1179 List<? extends IBase> values = nextChild.getAccessor().getValues(theElement); 1180 if (values != null) { 1181 for (IBase nextValue : values) { 1182 if (nextValue == null) { 1183 continue; 1184 } 1185 if (nextValue.isEmpty()) { 1186 continue; 1187 } 1188 BaseRuntimeElementDefinition<?> childElementDef; 1189 Class<? extends IBase> valueType = nextValue.getClass(); 1190 childElementDef = nextChild.getChildElementDefinitionByDatatype(valueType); 1191 while (childElementDef == null && IBase.class.isAssignableFrom(valueType)) { 1192 childElementDef = nextChild.getChildElementDefinitionByDatatype(valueType); 1193 valueType = (Class<? extends IBase>) valueType.getSuperclass(); 1194 } 1195 1196 Class<? extends IBase> typeClass = nextValue.getClass(); 1197 while (childElementDef == null && IBase.class.isAssignableFrom(typeClass)) { 1198 //noinspection unchecked 1199 typeClass = (Class<? extends IBase>) typeClass.getSuperclass(); 1200 childElementDef = nextChild.getChildElementDefinitionByDatatype(typeClass); 1201 } 1202 1203 Validate.notNull( 1204 childElementDef, 1205 "Found value of type[%s] which is not valid for field[%s] in %s", 1206 nextValue.getClass(), 1207 nextChild.getElementName(), 1208 childDef.getName()); 1209 1210 visit( 1211 nextValue, 1212 nextChild, 1213 childElementDef, 1214 theCallback, 1215 theContainingElementPath, 1216 theChildDefinitionPath, 1217 theElementDefinitionPath); 1218 } 1219 } 1220 } 1221 break; 1222 } 1223 case CONTAINED_RESOURCES: { 1224 BaseContainedDt value = (BaseContainedDt) theElement; 1225 for (IResource next : value.getContainedResources()) { 1226 BaseRuntimeElementCompositeDefinition<?> def = myContext.getResourceDefinition(next); 1227 visit( 1228 next, 1229 null, 1230 def, 1231 theCallback, 1232 theContainingElementPath, 1233 theChildDefinitionPath, 1234 theElementDefinitionPath); 1235 } 1236 break; 1237 } 1238 case EXTENSION_DECLARED: 1239 case UNDECL_EXT: { 1240 throw new IllegalStateException( 1241 Msg.code(1793) + "state should not happen: " + theDefinition.getChildType()); 1242 } 1243 case CONTAINED_RESOURCE_LIST: { 1244 if (theElement != null) { 1245 BaseRuntimeElementDefinition<?> def = myContext.getElementDefinition(theElement.getClass()); 1246 visit( 1247 theElement, 1248 null, 1249 def, 1250 theCallback, 1251 theContainingElementPath, 1252 theChildDefinitionPath, 1253 theElementDefinitionPath); 1254 } 1255 break; 1256 } 1257 } 1258 } 1259 1260 if (theChildDefinition != null) { 1261 theChildDefinitionPath.remove(theChildDefinitionPath.size() - 1); 1262 } 1263 theContainingElementPath.remove(theContainingElementPath.size() - 1); 1264 theElementDefinitionPath.remove(theElementDefinitionPath.size() - 1); 1265 } 1266 1267 /** 1268 * Visit all elements in a given resource 1269 * 1270 * <p> 1271 * Note on scope: This method will descend into any contained resources ({@link IResource#getContained()}) as well, but will not descend into linked resources (e.g. 1272 * {@link BaseResourceReferenceDt#getResource()}) or embedded resources (e.g. Bundle.entry.resource) 1273 * </p> 1274 * 1275 * @param theResource The resource to visit 1276 * @param theVisitor The visitor 1277 */ 1278 public void visit(IBaseResource theResource, IModelVisitor theVisitor) { 1279 BaseRuntimeElementCompositeDefinition<?> def = myContext.getResourceDefinition(theResource); 1280 visit(newMap(), theResource, theResource, null, null, def, theVisitor); 1281 } 1282 1283 public Map<Object, Object> newMap() { 1284 return new IdentityHashMap<>(); 1285 } 1286 1287 /** 1288 * Visit all elements in a given resource or element 1289 * <p> 1290 * <b>THIS ALTERNATE METHOD IS STILL EXPERIMENTAL! USE WITH CAUTION</b> 1291 * </p> 1292 * <p> 1293 * Note on scope: This method will descend into any contained resources ({@link IResource#getContained()}) as well, but will not descend into linked resources (e.g. 1294 * {@link BaseResourceReferenceDt#getResource()}) or embedded resources (e.g. Bundle.entry.resource) 1295 * </p> 1296 * 1297 * @param theElement The element to visit 1298 * @param theVisitor The visitor 1299 */ 1300 public void visit(IBase theElement, IModelVisitor2 theVisitor) { 1301 BaseRuntimeElementDefinition<?> def = myContext.getElementDefinition(theElement.getClass()); 1302 if (def instanceof BaseRuntimeElementCompositeDefinition) { 1303 visit(theElement, null, def, theVisitor, new ArrayList<>(), new ArrayList<>(), new ArrayList<>()); 1304 } else if (theElement instanceof IBaseExtension) { 1305 theVisitor.acceptUndeclaredExtension( 1306 (IBaseExtension<?, ?>) theElement, 1307 Collections.emptyList(), 1308 Collections.emptyList(), 1309 Collections.emptyList()); 1310 } else { 1311 theVisitor.acceptElement( 1312 theElement, Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); 1313 } 1314 } 1315 1316 private void visit( 1317 Map<Object, Object> theStack, 1318 IBaseResource theResource, 1319 IBase theElement, 1320 List<String> thePathToElement, 1321 BaseRuntimeChildDefinition theChildDefinition, 1322 BaseRuntimeElementDefinition<?> theDefinition, 1323 IModelVisitor theCallback) { 1324 List<String> pathToElement = addNameToList(thePathToElement, theChildDefinition); 1325 1326 if (theStack.put(theElement, theElement) != null) { 1327 return; 1328 } 1329 1330 theCallback.acceptElement(theResource, theElement, pathToElement, theChildDefinition, theDefinition); 1331 1332 BaseRuntimeElementDefinition<?> def = theDefinition; 1333 if (def.getChildType() == ChildTypeEnum.CONTAINED_RESOURCE_LIST) { 1334 Class<? extends IBase> clazz = theElement.getClass(); 1335 def = myContext.getElementDefinition(clazz); 1336 Validate.notNull(def, "Unable to find element definition for class: %s", clazz); 1337 } 1338 1339 if (theElement instanceof IBaseReference) { 1340 IBaseResource target = ((IBaseReference) theElement).getResource(); 1341 if (target != null) { 1342 if (target.getIdElement().hasIdPart() == false 1343 || target.getIdElement().isLocal()) { 1344 RuntimeResourceDefinition targetDef = myContext.getResourceDefinition(target); 1345 visit(theStack, target, target, pathToElement, null, targetDef, theCallback); 1346 } 1347 } 1348 } 1349 1350 switch (def.getChildType()) { 1351 case ID_DATATYPE: 1352 case PRIMITIVE_XHTML_HL7ORG: 1353 case PRIMITIVE_XHTML: 1354 case PRIMITIVE_DATATYPE: 1355 // These are primitive types 1356 break; 1357 case RESOURCE: 1358 case RESOURCE_BLOCK: 1359 case COMPOSITE_DATATYPE: { 1360 BaseRuntimeElementCompositeDefinition<?> childDef = (BaseRuntimeElementCompositeDefinition<?>) def; 1361 List<BaseRuntimeChildDefinition> childrenAndExtensionDefs = childDef.getChildrenAndExtension(); 1362 for (BaseRuntimeChildDefinition nextChild : childrenAndExtensionDefs) { 1363 1364 List<?> values = nextChild.getAccessor().getValues(theElement); 1365 1366 if (values != null) { 1367 for (Object nextValueObject : values) { 1368 IBase nextValue; 1369 try { 1370 nextValue = (IBase) nextValueObject; 1371 } catch (ClassCastException e) { 1372 String s = "Found instance of " + nextValueObject.getClass() 1373 + " - Did you set a field value to the incorrect type? Expected " 1374 + IBase.class.getName(); 1375 throw new ClassCastException(Msg.code(1794) + s); 1376 } 1377 if (nextValue == null) { 1378 continue; 1379 } 1380 if (nextValue.isEmpty()) { 1381 continue; 1382 } 1383 BaseRuntimeElementDefinition<?> childElementDef; 1384 Class<? extends IBase> clazz = nextValue.getClass(); 1385 childElementDef = nextChild.getChildElementDefinitionByDatatype(clazz); 1386 1387 if (childElementDef == null) { 1388 childElementDef = myContext.getElementDefinition(clazz); 1389 Validate.notNull( 1390 childElementDef, "Unable to find element definition for class: %s", clazz); 1391 } 1392 1393 if (nextChild instanceof RuntimeChildDirectResource) { 1394 // Don't descend into embedded resources 1395 theCallback.acceptElement(theResource, nextValue, null, nextChild, childElementDef); 1396 } else { 1397 visit( 1398 theStack, 1399 theResource, 1400 nextValue, 1401 pathToElement, 1402 nextChild, 1403 childElementDef, 1404 theCallback); 1405 } 1406 } 1407 } 1408 } 1409 break; 1410 } 1411 case CONTAINED_RESOURCES: { 1412 BaseContainedDt value = (BaseContainedDt) theElement; 1413 for (IResource next : value.getContainedResources()) { 1414 def = myContext.getResourceDefinition(next); 1415 visit(theStack, next, next, pathToElement, null, def, theCallback); 1416 } 1417 break; 1418 } 1419 case CONTAINED_RESOURCE_LIST: 1420 case EXTENSION_DECLARED: 1421 case UNDECL_EXT: { 1422 throw new IllegalStateException(Msg.code(1795) + "state should not happen: " + def.getChildType()); 1423 } 1424 } 1425 1426 theStack.remove(theElement); 1427 } 1428 1429 /** 1430 * Returns all embedded resources that are found embedded within <code>theResource</code>. 1431 * An embedded resource is a resource that can be found as a direct child within a resource, 1432 * as opposed to being referenced by the resource. 1433 * <p> 1434 * Examples include resources found within <code>Bundle.entry.resource</code> 1435 * and <code>Parameters.parameter.resource</code>, as well as contained resources 1436 * found within <code>Resource.contained</code> 1437 * </p> 1438 * 1439 * @param theRecurse Should embedded resources be recursively scanned for further embedded 1440 * resources 1441 * @return A collection containing the embedded resources. Order is arbitrary. 1442 */ 1443 public Collection<IBaseResource> getAllEmbeddedResources(IBaseResource theResource, boolean theRecurse) { 1444 Validate.notNull(theResource, "theResource must not be null"); 1445 ArrayList<IBaseResource> retVal = new ArrayList<>(); 1446 1447 visit(theResource, new IModelVisitor2() { 1448 @Override 1449 public boolean acceptElement( 1450 IBase theElement, 1451 List<IBase> theContainingElementPath, 1452 List<BaseRuntimeChildDefinition> theChildDefinitionPath, 1453 List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) { 1454 if (theElement == theResource) { 1455 return true; 1456 } 1457 if (theElement instanceof IBaseResource) { 1458 retVal.add((IBaseResource) theElement); 1459 return theRecurse; 1460 } 1461 return true; 1462 } 1463 1464 @Override 1465 public boolean acceptUndeclaredExtension( 1466 IBaseExtension<?, ?> theNextExt, 1467 List<IBase> theContainingElementPath, 1468 List<BaseRuntimeChildDefinition> theChildDefinitionPath, 1469 List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) { 1470 return true; 1471 } 1472 }); 1473 1474 return retVal; 1475 } 1476 1477 /** 1478 * Clear all content on a resource 1479 */ 1480 public void clear(IBaseResource theInput) { 1481 visit(theInput, new IModelVisitor2() { 1482 @Override 1483 public boolean acceptElement( 1484 IBase theElement, 1485 List<IBase> theContainingElementPath, 1486 List<BaseRuntimeChildDefinition> theChildDefinitionPath, 1487 List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) { 1488 if (theElement instanceof IPrimitiveType) { 1489 ((IPrimitiveType) theElement).setValueAsString(null); 1490 } 1491 return true; 1492 } 1493 1494 @Override 1495 public boolean acceptUndeclaredExtension( 1496 IBaseExtension<?, ?> theNextExt, 1497 List<IBase> theContainingElementPath, 1498 List<BaseRuntimeChildDefinition> theChildDefinitionPath, 1499 List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) { 1500 theNextExt.setUrl(null); 1501 theNextExt.setValue(null); 1502 return true; 1503 } 1504 }); 1505 } 1506 1507 private void containResourcesForEncoding(ContainedResources theContained, IBaseResource theResource) { 1508 List<IBaseReference> allReferences = getAllPopulatedChildElementsOfType(theResource, IBaseReference.class); 1509 1510 // Note that we process all contained resources that have arrived here with an ID contained resources first, so 1511 // that we don't accidentally auto-assign an ID 1512 // which may collide with a resource we have yet to process. 1513 // See: https://github.com/hapifhir/hapi-fhir/issues/6403 1514 allReferences.sort(REFERENCES_WITH_IDS_FIRST); 1515 1516 for (IBaseReference next : allReferences) { 1517 IBaseResource resource = next.getResource(); 1518 if (resource == null && next.getReferenceElement().isLocal()) { 1519 if (theContained.hasExistingIdToContainedResource()) { 1520 IBaseResource potentialTarget = theContained 1521 .getExistingIdToContainedResource() 1522 .remove(next.getReferenceElement().getValue()); 1523 if (potentialTarget != null) { 1524 theContained.addContained(next.getReferenceElement(), potentialTarget); 1525 containResourcesForEncoding(theContained, potentialTarget); 1526 } 1527 } 1528 } 1529 } 1530 1531 for (IBaseReference next : allReferences) { 1532 IBaseResource resource = next.getResource(); 1533 if (resource != null) { 1534 if (resource.getIdElement().isEmpty() || resource.getIdElement().isLocal()) { 1535 1536 IIdType id = theContained.addContained(resource); 1537 if (id == null) { 1538 continue; 1539 } 1540 getContainedResourceList(theResource).add(resource); 1541 1542 String idString = id.getValue(); 1543 if (!idString.startsWith("#")) { 1544 idString = "#" + idString; 1545 } 1546 1547 next.setReference(idString); 1548 next.setResource(null); 1549 if (resource.getIdElement().isLocal() && theContained.hasExistingIdToContainedResource()) { 1550 theContained 1551 .getExistingIdToContainedResource() 1552 .remove(resource.getIdElement().getValue()); 1553 } 1554 } else { 1555 IIdType previouslyContainedResourceId = theContained.getPreviouslyContainedResourceId(resource); 1556 if (previouslyContainedResourceId != null) { 1557 if (theContained.getResourceId(resource) == null) { 1558 theContained.addContained(previouslyContainedResourceId, resource); 1559 getContainedResourceList(theResource).add(resource); 1560 } 1561 } 1562 } 1563 } 1564 } 1565 } 1566 1567 /** 1568 * Iterate through the whole resource and identify any contained resources. Optionally this method 1569 * can also assign IDs and modify references where the resource link has been specified but not the 1570 * reference text. 1571 * 1572 * @since 5.4.0 1573 */ 1574 public ContainedResources containResources( 1575 IBaseResource theResource, ContainedResources theParentContainedResources, boolean theStoreResults) { 1576 1577 if (theStoreResults) { 1578 Object cachedValue = theResource.getUserData(USER_DATA_KEY_CONTAIN_RESOURCES_COMPLETED); 1579 if (cachedValue != null) { 1580 return (ContainedResources) cachedValue; 1581 } 1582 } 1583 1584 ContainedResources contained = new ContainedResources(); 1585 1586 List<? extends IBaseResource> containedResources = getContainedResourceList(theResource); 1587 for (IBaseResource next : containedResources) { 1588 String nextId = next.getIdElement().getValue(); 1589 if (StringUtils.isNotBlank(nextId)) { 1590 while (nextId.startsWith("#")) { 1591 ourLog.warn( 1592 "Found contained resource with ID starting with # character ({}). This form of ID is deprecated and will be dropped in a future release, preventing the current code from working correctly.", 1593 nextId); 1594 nextId = nextId.substring(1); 1595 } 1596 next.getIdElement().setValue(nextId); 1597 } 1598 contained.addContained(next); 1599 } 1600 1601 if (myContext.getParserOptions().isAutoContainReferenceTargetsWithNoId()) { 1602 if (theParentContainedResources != null) { 1603 if (theParentContainedResources.hasResourceToIdValues()) { 1604 contained 1605 .getPreviouslyContainedResourceToIdMap() 1606 .putAll(theParentContainedResources.getResourceToIdMap()); 1607 } 1608 if (theParentContainedResources.hasPreviouslyContainedResourceToIdValues()) { 1609 contained 1610 .getPreviouslyContainedResourceToIdMap() 1611 .putAll(theParentContainedResources.getPreviouslyContainedResourceToIdMap()); 1612 } 1613 } 1614 1615 containResourcesForEncoding(contained, theResource); 1616 } 1617 1618 if (theStoreResults) { 1619 theResource.setUserData(USER_DATA_KEY_CONTAIN_RESOURCES_COMPLETED, contained); 1620 } 1621 1622 return contained; 1623 } 1624 1625 @SuppressWarnings("unchecked") 1626 private <T extends IBaseResource> List<T> getContainedResourceList(T theResource) { 1627 List<T> containedResources = Collections.emptyList(); 1628 if (theResource instanceof IResource) { 1629 containedResources = 1630 (List<T>) ((IResource) theResource).getContained().getContainedResources(); 1631 } else if (theResource instanceof IDomainResource) { 1632 containedResources = (List<T>) ((IDomainResource) theResource).getContained(); 1633 } 1634 return containedResources; 1635 } 1636 1637 /** 1638 * Adds and returns a new element at the given path within the given structure. The paths used here 1639 * are <b>not FHIRPath expressions</b> but instead just simple dot-separated path expressions. 1640 * <p> 1641 * Only the last entry in the path is always created, existing repetitions of elements before 1642 * the final dot are returned if they exists (although they are created if they do not). For example, 1643 * given the path <code>Patient.name.given</code>, a new repetition of <code>given</code> is always 1644 * added to the first (index 0) repetition of the name. If an index-0 repetition of <code>name</code> 1645 * already exists, it is added to. If one does not exist, it if created and then added to. 1646 * </p> 1647 * <p> 1648 * If the last element in the path refers to a non-repeatable element that is already present and 1649 * is not empty, a {@link DataFormatException} error will be thrown. 1650 * </p> 1651 * 1652 * @param theTarget The element to add to. This will often be a {@link IBaseResource resource} 1653 * instance, but does not need to be. 1654 * @param thePath The path. 1655 * @return The newly added element 1656 * @throws DataFormatException If the path is invalid or does not end with either a repeatable element, or 1657 * an element that is non-repeatable but not already populated. 1658 */ 1659 @SuppressWarnings("unchecked") 1660 @Nonnull 1661 public <T extends IBase> T addElement(@Nonnull IBase theTarget, @Nonnull String thePath) { 1662 return (T) doAddElement(theTarget, thePath, 1).get(0); 1663 } 1664 1665 @SuppressWarnings("unchecked") 1666 private <T extends IBase> List<T> doAddElement(IBase theTarget, String thePath, int theElementsToAdd) { 1667 if (theElementsToAdd == 0) { 1668 return Collections.emptyList(); 1669 } 1670 1671 IBase target = theTarget; 1672 BaseRuntimeElementCompositeDefinition<?> def = 1673 (BaseRuntimeElementCompositeDefinition<?>) myContext.getElementDefinition(target.getClass()); 1674 List<String> parts = parsePath(def, thePath); 1675 1676 for (int i = 0, partsSize = parts.size(); ; i++) { 1677 String nextPart = parts.get(i); 1678 boolean lastPart = i == partsSize - 1; 1679 1680 BaseRuntimeChildDefinition nextChild = def.getChildByName(nextPart); 1681 if (nextChild == null) { 1682 throw new DataFormatException(Msg.code(1796) + "Invalid path " + thePath + ": Element of type " 1683 + def.getName() + " has no child named " + nextPart + ". Valid names: " 1684 + def.getChildrenAndExtension().stream() 1685 .map(BaseRuntimeChildDefinition::getElementName) 1686 .sorted() 1687 .collect(Collectors.joining(", "))); 1688 } 1689 1690 List<IBase> childValues = nextChild.getAccessor().getValues(target); 1691 IBase childValue; 1692 if (childValues.size() > 0 && !lastPart) { 1693 childValue = childValues.get(0); 1694 } else { 1695 1696 if (lastPart) { 1697 if (!childValues.isEmpty()) { 1698 if (theElementsToAdd == -1) { 1699 return (List<T>) Collections.singletonList(childValues.get(0)); 1700 } else if (nextChild.getMax() == 1 1701 && !childValues.get(0).isEmpty()) { 1702 throw new DataFormatException( 1703 Msg.code(1797) + "Element at path " + thePath + " is not repeatable and not empty"); 1704 } else if (nextChild.getMax() == 1 && childValues.get(0).isEmpty()) { 1705 return (List<T>) Collections.singletonList(childValues.get(0)); 1706 } 1707 } 1708 } 1709 1710 BaseRuntimeElementDefinition<?> elementDef = nextChild.getChildByName(nextPart); 1711 childValue = elementDef.newInstance(nextChild.getInstanceConstructorArguments()); 1712 nextChild.getMutator().addValue(target, childValue); 1713 1714 if (lastPart) { 1715 if (theElementsToAdd == 1 || theElementsToAdd == -1) { 1716 return (List<T>) Collections.singletonList(childValue); 1717 } else { 1718 if (nextChild.getMax() == 1) { 1719 throw new DataFormatException(Msg.code(1798) + "Can not add multiple values at path " 1720 + thePath + ": Element does not repeat"); 1721 } 1722 1723 List<T> values = (List<T>) Lists.newArrayList(childValue); 1724 for (int j = 1; j < theElementsToAdd; j++) { 1725 childValue = elementDef.newInstance(nextChild.getInstanceConstructorArguments()); 1726 nextChild.getMutator().addValue(target, childValue); 1727 values.add((T) childValue); 1728 } 1729 1730 return values; 1731 } 1732 } 1733 } 1734 1735 target = childValue; 1736 1737 if (!lastPart) { 1738 BaseRuntimeElementDefinition<?> nextDef = myContext.getElementDefinition(target.getClass()); 1739 if (!(nextDef instanceof BaseRuntimeElementCompositeDefinition)) { 1740 throw new DataFormatException(Msg.code(1799) + "Invalid path " + thePath + ": Element of type " 1741 + def.getName() + " has no child named " + nextPart + " (this is a primitive type)"); 1742 } 1743 def = (BaseRuntimeElementCompositeDefinition<?>) nextDef; 1744 } 1745 } 1746 } 1747 1748 /** 1749 * Adds and returns a new element at the given path within the given structure. The paths used here 1750 * are <b>not FHIRPath expressions</b> but instead just simple dot-separated path expressions. 1751 * <p> 1752 * This method follows all of the same semantics as {@link #addElement(IBase, String)} but it 1753 * requires the path to point to an element with a primitive datatype and set the value of 1754 * the datatype to the given value. 1755 * </p> 1756 * 1757 * @param theTarget The element to add to. This will often be a {@link IBaseResource resource} 1758 * instance, but does not need to be. 1759 * @param thePath The path. 1760 * @param theValue The value to set, or <code>null</code>. 1761 * @return The newly added element 1762 * @throws DataFormatException If the path is invalid or does not end with either a repeatable element, or 1763 * an element that is non-repeatable but not already populated. 1764 */ 1765 @SuppressWarnings("unchecked") 1766 @Nonnull 1767 public <T extends IBase> T addElement( 1768 @Nonnull IBase theTarget, @Nonnull String thePath, @Nullable String theValue) { 1769 T value = (T) doAddElement(theTarget, thePath, 1).get(0); 1770 if (!(value instanceof IPrimitiveType)) { 1771 throw new DataFormatException( 1772 Msg.code(1800) + "Element at path " + thePath + " is not a primitive datatype. Found: " 1773 + myContext.getElementDefinition(value.getClass()).getName()); 1774 } 1775 1776 ((IPrimitiveType<?>) value).setValueAsString(theValue); 1777 1778 return value; 1779 } 1780 1781 /** 1782 * Adds and returns a new element at the given path within the given structure. The paths used here 1783 * are <b>not FHIRPath expressions</b> but instead just simple dot-separated path expressions. 1784 * <p> 1785 * This method follows all of the same semantics as {@link #addElement(IBase, String)} but it 1786 * requires the path to point to an element with a primitive datatype and set the value of 1787 * the datatype to the given value. 1788 * </p> 1789 * 1790 * @param theTarget The element to add to. This will often be a {@link IBaseResource resource} 1791 * instance, but does not need to be. 1792 * @param thePath The path. 1793 * @param theValue The value to set, or <code>null</code>. 1794 * @return The newly added element 1795 * @throws DataFormatException If the path is invalid or does not end with either a repeatable element, or 1796 * an element that is non-repeatable but not already populated. 1797 */ 1798 @SuppressWarnings("unchecked") 1799 @Nonnull 1800 public <T extends IBase> T setElement( 1801 @Nonnull IBase theTarget, @Nonnull String thePath, @Nullable String theValue) { 1802 T value = (T) doAddElement(theTarget, thePath, -1).get(0); 1803 if (!(value instanceof IPrimitiveType)) { 1804 throw new DataFormatException( 1805 Msg.code(1801) + "Element at path " + thePath + " is not a primitive datatype. Found: " 1806 + myContext.getElementDefinition(value.getClass()).getName()); 1807 } 1808 1809 ((IPrimitiveType<?>) value).setValueAsString(theValue); 1810 1811 return value; 1812 } 1813 1814 /** 1815 * This method has the same semantics as {@link #addElement(IBase, String, String)} but adds 1816 * a collection of primitives instead of a single one. 1817 * 1818 * @param theTarget The element to add to. This will often be a {@link IBaseResource resource} 1819 * instance, but does not need to be. 1820 * @param thePath The path. 1821 * @param theValues The values to set, or <code>null</code>. 1822 */ 1823 public void addElements(IBase theTarget, String thePath, Collection<String> theValues) { 1824 List<IBase> targets = doAddElement(theTarget, thePath, theValues.size()); 1825 Iterator<String> valuesIter = theValues.iterator(); 1826 for (IBase target : targets) { 1827 1828 if (!(target instanceof IPrimitiveType)) { 1829 throw new DataFormatException(Msg.code(1802) + "Element at path " + thePath 1830 + " is not a primitive datatype. Found: " 1831 + myContext.getElementDefinition(target.getClass()).getName()); 1832 } 1833 1834 ((IPrimitiveType<?>) target).setValueAsString(valuesIter.next()); 1835 } 1836 } 1837 1838 /** 1839 * Checks if the field exists on the resource 1840 * 1841 * @param theFieldName Name of the field to check 1842 * @param theResource Resource instance to check 1843 * @return Returns true if resource definition has a child with the specified name and false otherwise 1844 */ 1845 public boolean fieldExists(String theFieldName, IBaseResource theResource) { 1846 return myContext.getResourceDefinition(theResource).getChildByName(theFieldName) != null; 1847 } 1848 1849 /** 1850 * Clones a resource object, copying all data elements from theSource into a new copy of the same type. 1851 * <p> 1852 * Note that: 1853 * <ul> 1854 * <li>Only FHIR data elements are copied (i.e. user data maps are not copied)</li> 1855 * <li>If a class extending a HAPI FHIR type (e.g. an instance of a class extending the Patient class) is supplied, an instance of the base type will be returned.</li> 1856 * </ul> 1857 * 1858 * @param theSource The source resource 1859 * @return A copy of the source resource 1860 * @since 5.6.0 1861 */ 1862 @SuppressWarnings("unchecked") 1863 public <T extends IBaseResource> T clone(T theSource) { 1864 Validate.notNull(theSource, "theSource must not be null"); 1865 T target = (T) myContext.getResourceDefinition(theSource).newInstance(); 1866 cloneInto(theSource, target, false); 1867 return target; 1868 } 1869 1870 @FunctionalInterface 1871 private interface ICompartmentOwnerVisitor { 1872 1873 /** 1874 * @return Returns true if we should keep looking for more 1875 */ 1876 boolean consume(IIdType theCompartmentOwner); 1877 } 1878 1879 public static class ContainedResources { 1880 private List<IBaseResource> myResourceList; 1881 private IdentityHashMap<IBaseResource, IIdType> myResourceToIdMap; 1882 private IdentityHashMap<IBaseResource, IIdType> myPreviouslyContainedResourceToIdMap; 1883 private Map<String, IBaseResource> myExistingIdToContainedResourceMap; 1884 1885 public ContainedResources() { 1886 super(); 1887 } 1888 1889 public Map<String, IBaseResource> getExistingIdToContainedResource() { 1890 if (myExistingIdToContainedResourceMap == null) { 1891 myExistingIdToContainedResourceMap = new HashMap<>(); 1892 } 1893 return myExistingIdToContainedResourceMap; 1894 } 1895 1896 public boolean referenceMatchesAContainedResource(IIdType theRefId) { 1897 assert theRefId.getValue().startsWith("#"); 1898 1899 String expectedResourceId = theRefId.getValue().substring(1); 1900 return this.getContainedResources().stream() 1901 .anyMatch(res -> res.getIdElement().getIdPart().equals(expectedResourceId)); 1902 } 1903 1904 public IIdType addContained(IBaseResource theResource) { 1905 if (this.getResourceId(theResource) != null) { 1906 // Prevent infinite recursion if there are circular loops in the contained resources 1907 return null; 1908 } 1909 1910 IIdType existing = getResourceToIdMap().get(theResource); 1911 if (existing != null) { 1912 return existing; 1913 } 1914 1915 IIdType newId = theResource.getIdElement(); 1916 if (isBlank(newId.getValue())) { 1917 newId.setValue(UUID.randomUUID().toString()); 1918 } else if (newId.getValue().startsWith("#")) { 1919 newId.setValue(newId.getValueAsString().substring(1)); 1920 } 1921 1922 getResourceToIdMap().put(theResource, newId); 1923 getOrCreateResourceList().add(theResource); 1924 return newId; 1925 } 1926 1927 public void addContained(IIdType theId, IBaseResource theResource) { 1928 if (!getResourceToIdMap().containsKey(theResource)) { 1929 getResourceToIdMap().put(theResource, theId); 1930 getOrCreateResourceList().add(theResource); 1931 } 1932 } 1933 1934 public List<IBaseResource> getContainedResources() { 1935 if (getResourceToIdMap() == null) { 1936 return Collections.emptyList(); 1937 } 1938 return getOrCreateResourceList(); 1939 } 1940 1941 public IIdType getResourceId(IBaseResource theNext) { 1942 if (getResourceToIdMap() == null) { 1943 return null; 1944 } 1945 1946 var idFromMap = getResourceToIdMap().get(theNext); 1947 if (idFromMap != null) { 1948 return idFromMap; 1949 } else if (theNext.getIdElement().getIdPart() != null) { 1950 return getResourceToIdMap().values().stream() 1951 .filter(id -> theNext.getIdElement().getIdPart().equals(id.getIdPart())) 1952 .findAny() 1953 .orElse(null); 1954 } else { 1955 return null; 1956 } 1957 } 1958 1959 private List<IBaseResource> getOrCreateResourceList() { 1960 if (myResourceList == null) { 1961 myResourceList = new ArrayList<>(); 1962 } 1963 return myResourceList; 1964 } 1965 1966 private boolean hasResourceToIdValues() { 1967 return myResourceToIdMap != null && !myResourceToIdMap.isEmpty(); 1968 } 1969 1970 private IdentityHashMap<IBaseResource, IIdType> getResourceToIdMap() { 1971 if (myResourceToIdMap == null) { 1972 myResourceToIdMap = new IdentityHashMap<>(); 1973 } 1974 return myResourceToIdMap; 1975 } 1976 1977 public boolean isEmpty() { 1978 if (myResourceToIdMap == null) { 1979 return true; 1980 } 1981 return myResourceToIdMap.isEmpty(); 1982 } 1983 1984 public boolean hasExistingIdToContainedResource() { 1985 return myExistingIdToContainedResourceMap != null; 1986 } 1987 1988 public IdentityHashMap<IBaseResource, IIdType> getPreviouslyContainedResourceToIdMap() { 1989 if (myPreviouslyContainedResourceToIdMap == null) { 1990 myPreviouslyContainedResourceToIdMap = new IdentityHashMap<>(); 1991 } 1992 return myPreviouslyContainedResourceToIdMap; 1993 } 1994 1995 public boolean hasPreviouslyContainedResourceToIdValues() { 1996 return myPreviouslyContainedResourceToIdMap != null && !myPreviouslyContainedResourceToIdMap.isEmpty(); 1997 } 1998 1999 public IIdType getPreviouslyContainedResourceId(IBaseResource theResource) { 2000 if (hasPreviouslyContainedResourceToIdValues()) { 2001 return getPreviouslyContainedResourceToIdMap().get(theResource); 2002 } 2003 return null; 2004 } 2005 } 2006}