ca.uhn.fhir.util.FhirTerser Maven / Gradle / Ivy
/*
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2024 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.util;
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementDefinition.ChildTypeEnum;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.RuntimeChildChoiceDefinition;
import ca.uhn.fhir.context.RuntimeChildDirectResource;
import ca.uhn.fhir.context.RuntimeExtensionDtDefinition;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.model.api.ExtensionDt;
import ca.uhn.fhir.model.api.IIdentifiableElement;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.ISupportsUndeclaredExtensions;
import ca.uhn.fhir.model.base.composite.BaseContainedDt;
import ca.uhn.fhir.model.base.composite.BaseResourceReferenceDt;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.parser.DataFormatException;
import com.google.common.collect.Lists;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseElement;
import org.hl7.fhir.instance.model.api.IBaseExtension;
import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
import org.hl7.fhir.instance.model.api.IBaseHasModifierExtensions;
import org.hl7.fhir.instance.model.api.IBaseReference;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IDomainResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.substring;
public class FhirTerser {
private static final Pattern COMPARTMENT_MATCHER_PATH =
Pattern.compile("([a-zA-Z.]+)\\.where\\(resolve\\(\\) is ([a-zA-Z]+)\\)");
private static final String USER_DATA_KEY_CONTAIN_RESOURCES_COMPLETED =
FhirTerser.class.getName() + "_CONTAIN_RESOURCES_COMPLETED";
private final FhirContext myContext;
public FhirTerser(FhirContext theContext) {
super();
myContext = theContext;
}
private List addNameToList(List theCurrentList, BaseRuntimeChildDefinition theChildDefinition) {
if (theChildDefinition == null) return null;
if (theCurrentList == null || theCurrentList.isEmpty())
return new ArrayList<>(Collections.singletonList(theChildDefinition.getElementName()));
List newList = new ArrayList<>(theCurrentList);
newList.add(theChildDefinition.getElementName());
return newList;
}
private ExtensionDt createEmptyExtensionDt(IBaseExtension theBaseExtension, String theUrl) {
return createEmptyExtensionDt(theBaseExtension, false, theUrl);
}
@SuppressWarnings("unchecked")
private ExtensionDt createEmptyExtensionDt(IBaseExtension theBaseExtension, boolean theIsModifier, String theUrl) {
ExtensionDt retVal = new ExtensionDt(theIsModifier, theUrl);
theBaseExtension.getExtension().add(retVal);
return retVal;
}
private ExtensionDt createEmptyExtensionDt(
ISupportsUndeclaredExtensions theSupportsUndeclaredExtensions, String theUrl) {
return createEmptyExtensionDt(theSupportsUndeclaredExtensions, false, theUrl);
}
private ExtensionDt createEmptyExtensionDt(
ISupportsUndeclaredExtensions theSupportsUndeclaredExtensions, boolean theIsModifier, String theUrl) {
return theSupportsUndeclaredExtensions.addUndeclaredExtension(theIsModifier, theUrl);
}
private IBaseExtension createEmptyExtension(IBaseHasExtensions theBaseHasExtensions, String theUrl) {
return (IBaseExtension) theBaseHasExtensions.addExtension().setUrl(theUrl);
}
private IBaseExtension createEmptyModifierExtension(
IBaseHasModifierExtensions theBaseHasModifierExtensions, String theUrl) {
return (IBaseExtension)
theBaseHasModifierExtensions.addModifierExtension().setUrl(theUrl);
}
private ExtensionDt createEmptyModifierExtensionDt(
ISupportsUndeclaredExtensions theSupportsUndeclaredExtensions, String theUrl) {
return createEmptyExtensionDt(theSupportsUndeclaredExtensions, true, theUrl);
}
/**
* Clones all values from a source object into the equivalent fields in a target object
*
* @param theSource The source object (must not be null)
* @param theTarget The target object to copy values into (must not be null)
* @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)
* @return Returns the target (which will be the same object that was passed into theTarget) for easy chaining
*/
public IBase cloneInto(IBase theSource, IBase theTarget, boolean theIgnoreMissingFields) {
Validate.notNull(theSource, "theSource must not be null");
Validate.notNull(theTarget, "theTarget must not be null");
// DSTU3+
if (theSource instanceof IBaseElement) {
IBaseElement source = (IBaseElement) theSource;
IBaseElement target = (IBaseElement) theTarget;
target.setId(source.getId());
}
// DSTU2 only
if (theSource instanceof IIdentifiableElement) {
IIdentifiableElement source = (IIdentifiableElement) theSource;
IIdentifiableElement target = (IIdentifiableElement) theTarget;
target.setElementSpecificId(source.getElementSpecificId());
}
// DSTU2 only
if (theSource instanceof IResource) {
IResource source = (IResource) theSource;
IResource target = (IResource) theTarget;
target.setId(source.getId());
target.getResourceMetadata().putAll(source.getResourceMetadata());
}
if (theSource instanceof IPrimitiveType>) {
if (theTarget instanceof IPrimitiveType>) {
String valueAsString = ((IPrimitiveType>) theSource).getValueAsString();
if (isNotBlank(valueAsString)) {
((IPrimitiveType>) theTarget).setValueAsString(valueAsString);
}
if (theSource instanceof IBaseHasExtensions && theTarget instanceof IBaseHasExtensions) {
List extends IBaseExtension, ?>> extensions = ((IBaseHasExtensions) theSource).getExtension();
for (IBaseExtension, ?> nextSource : extensions) {
IBaseExtension, ?> nextTarget = ((IBaseHasExtensions) theTarget).addExtension();
cloneInto(nextSource, nextTarget, theIgnoreMissingFields);
}
}
return theSource;
}
if (theIgnoreMissingFields) {
return theSource;
}
throw new DataFormatException(Msg.code(1788) + "Can not copy value from primitive of type "
+ theSource.getClass().getName() + " into type "
+ theTarget.getClass().getName());
}
BaseRuntimeElementCompositeDefinition> sourceDef =
(BaseRuntimeElementCompositeDefinition>) myContext.getElementDefinition(theSource.getClass());
BaseRuntimeElementCompositeDefinition> targetDef =
(BaseRuntimeElementCompositeDefinition>) myContext.getElementDefinition(theTarget.getClass());
List children = sourceDef.getChildren();
if (sourceDef instanceof RuntimeExtensionDtDefinition) {
children = ((RuntimeExtensionDtDefinition) sourceDef).getChildrenIncludingUrl();
}
for (BaseRuntimeChildDefinition nextChild : children)
for (IBase nextValue : nextChild.getAccessor().getValues(theSource)) {
Class extends IBase> valueType = nextValue.getClass();
String elementName = nextChild.getChildNameByDatatype(valueType);
BaseRuntimeChildDefinition targetChild = targetDef.getChildByName(elementName);
if (targetChild == null) {
if (theIgnoreMissingFields) {
continue;
}
throw new DataFormatException(Msg.code(1789) + "Type "
+ theTarget.getClass().getName() + " does not have a child with name " + elementName);
}
BaseRuntimeElementDefinition> element = myContext.getElementDefinition(valueType);
Object instanceConstructorArg = targetChild.getInstanceConstructorArguments();
IBase target;
if (element == null && BaseContainedDt.class.isAssignableFrom(valueType)) {
/*
* This is a hack for DSTU2 - The way we did contained resources in
* the DSTU2 model was weird, since the element isn't actually a FHIR type.
* This is fixed in DSTU3+ so this hack only applies there.
*/
BaseContainedDt containedTarget = (BaseContainedDt) ReflectionUtil.newInstance(valueType);
BaseContainedDt containedSource = (BaseContainedDt) nextValue;
for (IResource next : containedSource.getContainedResources()) {
List containedResources = containedTarget.getContainedResources();
containedResources.add(next);
}
targetChild.getMutator().addValue(theTarget, containedTarget);
continue;
} else if (instanceConstructorArg != null) {
target = element.newInstance(instanceConstructorArg);
} else {
target = element.newInstance();
}
targetChild.getMutator().addValue(theTarget, target);
cloneInto(nextValue, target, theIgnoreMissingFields);
}
return theTarget;
}
/**
* Returns a list containing all child elements (including the resource itself) which are non-empty and are either of the exact type specified, or are a subclass of that type.
*
* 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
* well as any contained resources.
*
*
* 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.
* {@link BaseResourceReferenceDt#getResource()}) or embedded resources (e.g. Bundle.entry.resource)
*
*
* @param theResource The resource instance to search. Must not be null.
* @param theType The type to search for. Must not be null.
* @return Returns a list of all matching elements
*/
public List getAllPopulatedChildElementsOfType(
IBaseResource theResource, final Class theType) {
final ArrayList retVal = new ArrayList<>();
BaseRuntimeElementCompositeDefinition> def = myContext.getResourceDefinition(theResource);
visit(newMap(), theResource, theResource, null, null, def, new IModelVisitor() {
@SuppressWarnings("unchecked")
@Override
public void acceptElement(
IBaseResource theOuterResource,
IBase theElement,
List thePathToElement,
BaseRuntimeChildDefinition theChildDefinition,
BaseRuntimeElementDefinition> theDefinition) {
if (theElement == null || theElement.isEmpty()) {
return;
}
if (theType.isAssignableFrom(theElement.getClass())) {
retVal.add((T) theElement);
}
}
});
return retVal;
}
/**
* Extracts all outbound references from a resource
*
* @param theResource the resource to be analyzed
* @return a list of references to other resources
*/
public List getAllResourceReferences(final IBaseResource theResource) {
return getAllResourceReferencesExcluding(theResource, Lists.newArrayList());
}
/**
* Extracts all outbound references from a resource, excluding any that are located on black-listed parts of the
* resource
*
* @param theResource the resource to be analyzed
* @param thePathsToExclude a list of dot-delimited paths not to include in the result
* @return a list of references to other resources
*/
public List getAllResourceReferencesExcluding(
final IBaseResource theResource, List thePathsToExclude) {
final ArrayList retVal = new ArrayList<>();
BaseRuntimeElementCompositeDefinition> def = myContext.getResourceDefinition(theResource);
List> tokenizedPathsToExclude = thePathsToExclude.stream()
.map(path -> StringUtils.split(path, "."))
.map(Lists::newArrayList)
.collect(Collectors.toList());
visit(newMap(), theResource, theResource, null, null, def, new IModelVisitor() {
@Override
public void acceptElement(
IBaseResource theOuterResource,
IBase theElement,
List thePathToElement,
BaseRuntimeChildDefinition theChildDefinition,
BaseRuntimeElementDefinition> theDefinition) {
if (theElement == null || theElement.isEmpty()) {
return;
}
if (thePathToElement != null && pathShouldBeExcluded(tokenizedPathsToExclude, thePathToElement)) {
return;
}
if (IBaseReference.class.isAssignableFrom(theElement.getClass())) {
retVal.add(new ResourceReferenceInfo(
myContext, theOuterResource, thePathToElement, (IBaseReference) theElement));
}
}
});
return retVal;
}
private boolean pathShouldBeExcluded(List> theTokenizedPathsToExclude, List thePathToElement) {
return theTokenizedPathsToExclude.stream().anyMatch(p -> {
// Check whether the path to the element starts with the path to be excluded
if (p.size() > thePathToElement.size()) {
return false;
}
List prefix = thePathToElement.subList(0, p.size());
return Objects.equals(p, prefix);
});
}
private BaseRuntimeChildDefinition getDefinition(
BaseRuntimeElementCompositeDefinition> theCurrentDef, List theSubList) {
BaseRuntimeChildDefinition nextDef = theCurrentDef.getChildByNameOrThrowDataFormatException(theSubList.get(0));
if (theSubList.size() == 1) {
return nextDef;
}
BaseRuntimeElementCompositeDefinition> cmp =
(BaseRuntimeElementCompositeDefinition>) nextDef.getChildByName(theSubList.get(0));
return getDefinition(cmp, theSubList.subList(1, theSubList.size()));
}
public BaseRuntimeChildDefinition getDefinition(Class extends IBaseResource> theResourceType, String thePath) {
RuntimeResourceDefinition def = myContext.getResourceDefinition(theResourceType);
List parts = Arrays.asList(thePath.split("\\."));
List subList = parts.subList(1, parts.size());
if (subList.size() < 1) {
throw new ConfigurationException(Msg.code(1790) + "Invalid path: " + thePath);
}
return getDefinition(def, subList);
}
public Object getSingleValueOrNull(IBase theTarget, String thePath) {
Class wantedType = IBase.class;
return getSingleValueOrNull(theTarget, thePath, wantedType);
}
public T getSingleValueOrNull(IBase theTarget, String thePath, Class theWantedType) {
Validate.notNull(theTarget, "theTarget must not be null");
Validate.notBlank(thePath, "thePath must not be empty");
BaseRuntimeElementDefinition> def = myContext.getElementDefinition(theTarget.getClass());
if (!(def instanceof BaseRuntimeElementCompositeDefinition)) {
throw new IllegalArgumentException(Msg.code(1791) + "Target is not a composite type: "
+ theTarget.getClass().getName());
}
BaseRuntimeElementCompositeDefinition> currentDef = (BaseRuntimeElementCompositeDefinition>) def;
List parts = parsePath(currentDef, thePath);
List retVal = getValues(currentDef, theTarget, parts, theWantedType);
if (retVal.isEmpty()) {
return null;
}
return retVal.get(0);
}
public Optional getSinglePrimitiveValue(IBase theTarget, String thePath) {
return getSingleValue(theTarget, thePath, IPrimitiveType.class).map(t -> t.getValueAsString());
}
public String getSinglePrimitiveValueOrNull(IBase theTarget, String thePath) {
return getSingleValue(theTarget, thePath, IPrimitiveType.class)
.map(t -> t.getValueAsString())
.orElse(null);
}
public Optional getSingleValue(IBase theTarget, String thePath, Class theWantedType) {
return Optional.ofNullable(getSingleValueOrNull(theTarget, thePath, theWantedType));
}
private List getValues(
BaseRuntimeElementCompositeDefinition> theCurrentDef,
IBase theCurrentObj,
List theSubList,
Class theWantedClass) {
return getValues(theCurrentDef, theCurrentObj, theSubList, theWantedClass, false, false);
}
@SuppressWarnings("unchecked")
private List getValues(
BaseRuntimeElementCompositeDefinition> theCurrentDef,
IBase theCurrentObj,
List theSubList,
Class theWantedClass,
boolean theCreate,
boolean theAddExtension) {
if (theSubList.isEmpty()) {
return Collections.emptyList();
}
String name = theSubList.get(0);
List retVal = new ArrayList<>();
if (name.startsWith("extension('")) {
String extensionUrl = name.substring("extension('".length());
int endIndex = extensionUrl.indexOf('\'');
if (endIndex != -1) {
extensionUrl = extensionUrl.substring(0, endIndex);
}
if (myContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
// DTSU2
final String extensionDtUrlForLambda = extensionUrl;
List extensionDts = Collections.emptyList();
if (theCurrentObj instanceof ISupportsUndeclaredExtensions) {
extensionDts = ((ISupportsUndeclaredExtensions) theCurrentObj)
.getUndeclaredExtensions().stream()
.filter(t -> t.getUrl().equals(extensionDtUrlForLambda))
.collect(Collectors.toList());
if (theAddExtension
&& (!(theCurrentObj instanceof IBaseExtension)
|| (extensionDts.isEmpty() && theSubList.size() == 1))) {
extensionDts.add(
createEmptyExtensionDt((ISupportsUndeclaredExtensions) theCurrentObj, extensionUrl));
}
if (extensionDts.isEmpty() && theCreate) {
extensionDts.add(
createEmptyExtensionDt((ISupportsUndeclaredExtensions) theCurrentObj, extensionUrl));
}
} else if (theCurrentObj instanceof IBaseExtension) {
extensionDts = ((IBaseExtension) theCurrentObj).getExtension();
if (theAddExtension && (extensionDts.isEmpty() && theSubList.size() == 1)) {
extensionDts.add(createEmptyExtensionDt((IBaseExtension) theCurrentObj, extensionUrl));
}
if (extensionDts.isEmpty() && theCreate) {
extensionDts.add(createEmptyExtensionDt((IBaseExtension) theCurrentObj, extensionUrl));
}
}
for (ExtensionDt next : extensionDts) {
if (theWantedClass.isAssignableFrom(next.getClass())) {
retVal.add((T) next);
}
}
} else {
// DSTU3+
final String extensionUrlForLambda = extensionUrl;
List extensions = Collections.emptyList();
if (theCurrentObj instanceof IBaseHasExtensions) {
extensions = ((IBaseHasExtensions) theCurrentObj)
.getExtension().stream()
.filter(t -> t.getUrl().equals(extensionUrlForLambda))
.collect(Collectors.toList());
if (theAddExtension
&& (!(theCurrentObj instanceof IBaseExtension)
|| (extensions.isEmpty() && theSubList.size() == 1))) {
extensions.add(createEmptyExtension((IBaseHasExtensions) theCurrentObj, extensionUrl));
}
if (extensions.isEmpty() && theCreate) {
extensions.add(createEmptyExtension((IBaseHasExtensions) theCurrentObj, extensionUrl));
}
}
for (IBaseExtension next : extensions) {
if (theWantedClass.isAssignableFrom(next.getClass())) {
retVal.add((T) next);
}
}
}
if (theSubList.size() > 1) {
List values = retVal;
retVal = new ArrayList<>();
for (T nextElement : values) {
BaseRuntimeElementCompositeDefinition> nextChildDef = (BaseRuntimeElementCompositeDefinition>)
myContext.getElementDefinition(nextElement.getClass());
List foundValues = getValues(
nextChildDef,
nextElement,
theSubList.subList(1, theSubList.size()),
theWantedClass,
theCreate,
theAddExtension);
retVal.addAll(foundValues);
}
}
return retVal;
}
if (name.startsWith("modifierExtension('")) {
String extensionUrl = name.substring("modifierExtension('".length());
int endIndex = extensionUrl.indexOf('\'');
if (endIndex != -1) {
extensionUrl = extensionUrl.substring(0, endIndex);
}
if (myContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
// DSTU2
final String extensionDtUrlForLambda = extensionUrl;
List extensionDts = Collections.emptyList();
if (theCurrentObj instanceof ISupportsUndeclaredExtensions) {
extensionDts = ((ISupportsUndeclaredExtensions) theCurrentObj)
.getUndeclaredModifierExtensions().stream()
.filter(t -> t.getUrl().equals(extensionDtUrlForLambda))
.collect(Collectors.toList());
if (theAddExtension
&& (!(theCurrentObj instanceof IBaseExtension)
|| (extensionDts.isEmpty() && theSubList.size() == 1))) {
extensionDts.add(createEmptyModifierExtensionDt(
(ISupportsUndeclaredExtensions) theCurrentObj, extensionUrl));
}
if (extensionDts.isEmpty() && theCreate) {
extensionDts.add(createEmptyModifierExtensionDt(
(ISupportsUndeclaredExtensions) theCurrentObj, extensionUrl));
}
} else if (theCurrentObj instanceof IBaseExtension) {
extensionDts = ((IBaseExtension) theCurrentObj).getExtension();
if (theAddExtension && (extensionDts.isEmpty() && theSubList.size() == 1)) {
extensionDts.add(createEmptyExtensionDt((IBaseExtension) theCurrentObj, extensionUrl));
}
if (extensionDts.isEmpty() && theCreate) {
extensionDts.add(createEmptyExtensionDt((IBaseExtension) theCurrentObj, extensionUrl));
}
}
for (ExtensionDt next : extensionDts) {
if (theWantedClass.isAssignableFrom(next.getClass())) {
retVal.add((T) next);
}
}
} else {
// DSTU3+
final String extensionUrlForLambda = extensionUrl;
List extensions = Collections.emptyList();
if (theCurrentObj instanceof IBaseHasModifierExtensions) {
extensions = ((IBaseHasModifierExtensions) theCurrentObj)
.getModifierExtension().stream()
.filter(t -> t.getUrl().equals(extensionUrlForLambda))
.collect(Collectors.toList());
if (theAddExtension
&& (!(theCurrentObj instanceof IBaseExtension)
|| (extensions.isEmpty() && theSubList.size() == 1))) {
extensions.add(
createEmptyModifierExtension((IBaseHasModifierExtensions) theCurrentObj, extensionUrl));
}
if (extensions.isEmpty() && theCreate) {
extensions.add(
createEmptyModifierExtension((IBaseHasModifierExtensions) theCurrentObj, extensionUrl));
}
}
for (IBaseExtension next : extensions) {
if (theWantedClass.isAssignableFrom(next.getClass())) {
retVal.add((T) next);
}
}
}
if (theSubList.size() > 1) {
List values = retVal;
retVal = new ArrayList<>();
for (T nextElement : values) {
BaseRuntimeElementCompositeDefinition> nextChildDef = (BaseRuntimeElementCompositeDefinition>)
myContext.getElementDefinition(nextElement.getClass());
List foundValues = getValues(
nextChildDef,
nextElement,
theSubList.subList(1, theSubList.size()),
theWantedClass,
theCreate,
theAddExtension);
retVal.addAll(foundValues);
}
}
return retVal;
}
BaseRuntimeChildDefinition nextDef = theCurrentDef.getChildByNameOrThrowDataFormatException(name);
List extends IBase> values = nextDef.getAccessor().getValues(theCurrentObj);
if (values.isEmpty() && theCreate) {
BaseRuntimeElementDefinition> childByName = nextDef.getChildByName(name);
Object arg = nextDef.getInstanceConstructorArguments();
IBase value;
if (arg != null) {
value = childByName.newInstance(arg);
} else {
value = childByName.newInstance();
}
nextDef.getMutator().addValue(theCurrentObj, value);
List list = new ArrayList<>();
list.add(value);
values = list;
}
if (theSubList.size() == 1) {
if (nextDef instanceof RuntimeChildChoiceDefinition) {
for (IBase next : values) {
if (next != null) {
if (name.endsWith("[x]")) {
if (theWantedClass == null || theWantedClass.isAssignableFrom(next.getClass())) {
retVal.add((T) next);
}
} else {
String childName = nextDef.getChildNameByDatatype(next.getClass());
if (theSubList.get(0).equals(childName)) {
if (theWantedClass == null || theWantedClass.isAssignableFrom(next.getClass())) {
retVal.add((T) next);
}
}
}
}
}
} else {
for (IBase next : values) {
if (next != null) {
if (theWantedClass == null || theWantedClass.isAssignableFrom(next.getClass())) {
retVal.add((T) next);
}
}
}
}
} else {
for (IBase nextElement : values) {
BaseRuntimeElementCompositeDefinition> nextChildDef = (BaseRuntimeElementCompositeDefinition>)
myContext.getElementDefinition(nextElement.getClass());
List foundValues = getValues(
nextChildDef,
nextElement,
theSubList.subList(1, theSubList.size()),
theWantedClass,
theCreate,
theAddExtension);
retVal.addAll(foundValues);
}
}
return retVal;
}
/**
* Returns values stored in an element identified by its path. The list of values is of
* type {@link Object}.
*
* @param theElement The element to be accessed. Must not be null.
* @param thePath The path for the element to be accessed.@param theElement The resource instance to be accessed. Must not be null.
* @return A list of values of type {@link Object}.
*/
public List getValues(IBase theElement, String thePath) {
Class wantedClass = IBase.class;
return getValues(theElement, thePath, wantedClass);
}
/**
* Returns values stored in an element identified by its path. The list of values is of
* type {@link Object}.
*
* @param theElement The element to be accessed. Must not be null.
* @param thePath The path for the element to be accessed.
* @param theCreate When set to true
, the terser will create a null-valued element where none exists.
* @return A list of values of type {@link Object}.
*/
public List getValues(IBase theElement, String thePath, boolean theCreate) {
Class wantedClass = IBase.class;
return getValues(theElement, thePath, wantedClass, theCreate);
}
/**
* Returns values stored in an element identified by its path. The list of values is of
* type {@link Object}.
*
* @param theElement The element to be accessed. Must not be null.
* @param thePath The path for the element to be accessed.
* @param theCreate When set to true
, the terser will create a null-valued element where none exists.
* @param theAddExtension When set to true
, the terser will add a null-valued extension where one or more such extensions already exist.
* @return A list of values of type {@link Object}.
*/
public List getValues(IBase theElement, String thePath, boolean theCreate, boolean theAddExtension) {
Class wantedClass = IBase.class;
return getValues(theElement, thePath, wantedClass, theCreate, theAddExtension);
}
/**
* Returns values stored in an element identified by its path. The list of values is of
* type theWantedClass
.
*
* @param theElement The element to be accessed. Must not be null.
* @param thePath The path for the element to be accessed.
* @param theWantedClass The desired class to be returned in a list.
* @param Type declared by theWantedClass
* @return A list of values of type theWantedClass
.
*/
public List getValues(IBase theElement, String thePath, Class theWantedClass) {
BaseRuntimeElementCompositeDefinition> def =
(BaseRuntimeElementCompositeDefinition>) myContext.getElementDefinition(theElement.getClass());
List parts = parsePath(def, thePath);
return getValues(def, theElement, parts, theWantedClass);
}
/**
* Returns values stored in an element identified by its path. The list of values is of
* type theWantedClass
.
*
* @param theElement The element to be accessed. Must not be null.
* @param thePath The path for the element to be accessed.
* @param theWantedClass The desired class to be returned in a list.
* @param theCreate When set to true
, the terser will create a null-valued element where none exists.
* @param Type declared by theWantedClass
* @return A list of values of type theWantedClass
.
*/
public List getValues(
IBase theElement, String thePath, Class theWantedClass, boolean theCreate) {
BaseRuntimeElementCompositeDefinition> def =
(BaseRuntimeElementCompositeDefinition>) myContext.getElementDefinition(theElement.getClass());
List parts = parsePath(def, thePath);
return getValues(def, theElement, parts, theWantedClass, theCreate, false);
}
/**
* Returns values stored in an element identified by its path. The list of values is of
* type theWantedClass
.
*
* @param theElement The element to be accessed. Must not be null.
* @param thePath The path for the element to be accessed.
* @param theWantedClass The desired class to be returned in a list.
* @param theCreate When set to true
, the terser will create a null-valued element where none exists.
* @param theAddExtension When set to true
, the terser will add a null-valued extension where one or more such extensions already exist.
* @param Type declared by theWantedClass
* @return A list of values of type theWantedClass
.
*/
public List getValues(
IBase theElement, String thePath, Class theWantedClass, boolean theCreate, boolean theAddExtension) {
BaseRuntimeElementCompositeDefinition> def =
(BaseRuntimeElementCompositeDefinition>) myContext.getElementDefinition(theElement.getClass());
List parts = parsePath(def, thePath);
return getValues(def, theElement, parts, theWantedClass, theCreate, theAddExtension);
}
private List parsePath(BaseRuntimeElementCompositeDefinition> theElementDef, String thePath) {
List parts = new ArrayList<>();
int currentStart = 0;
boolean inSingleQuote = false;
for (int i = 0; i < thePath.length(); i++) {
switch (thePath.charAt(i)) {
case '\'':
inSingleQuote = !inSingleQuote;
break;
case '.':
if (!inSingleQuote) {
parts.add(thePath.substring(currentStart, i));
currentStart = i + 1;
}
break;
}
}
parts.add(thePath.substring(currentStart));
String firstPart = parts.get(0);
if (Character.isUpperCase(firstPart.charAt(0)) && theElementDef instanceof RuntimeResourceDefinition) {
if (firstPart.equals(theElementDef.getName())) {
parts = parts.subList(1, parts.size());
} else {
parts = Collections.emptyList();
return parts;
}
} else if (firstPart.equals(theElementDef.getName())) {
parts = parts.subList(1, parts.size());
}
if (parts.size() < 1) {
throw new ConfigurationException(Msg.code(1792) + "Invalid path: " + thePath);
}
return parts;
}
/**
* Returns true
if theSource
is in the compartment named theCompartmentName
* belonging to resource theTarget
*
* @param theCompartmentName The name of the compartment
* @param theSource The potential member of the compartment
* @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}
* @return true
if theSource
is in the compartment
* @throws IllegalArgumentException If theTarget does not contain both a resource type and ID
*/
public boolean isSourceInCompartmentForTarget(
String theCompartmentName, IBaseResource theSource, IIdType theTarget) {
return isSourceInCompartmentForTarget(theCompartmentName, theSource, theTarget, null);
}
/**
* Returns true
if theSource
is in the compartment named theCompartmentName
* belonging to resource theTarget
*
* @param theCompartmentName The name of the compartment
* @param theSource The potential member of the compartment
* @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}
* @param theAdditionalCompartmentParamNames If provided, search param names provided here will be considered as included in the given compartment for this comparison.
* @return true
if theSource
is in the compartment or one of the additional parameters matched.
* @throws IllegalArgumentException If theTarget does not contain both a resource type and ID
*/
public boolean isSourceInCompartmentForTarget(
String theCompartmentName,
IBaseResource theSource,
IIdType theTarget,
Set theAdditionalCompartmentParamNames) {
Validate.notBlank(theCompartmentName, "theCompartmentName must not be null or blank");
Validate.notNull(theSource, "theSource must not be null");
Validate.notNull(theTarget, "theTarget must not be null");
Validate.notBlank(
defaultString(theTarget.getResourceType()),
"theTarget must have a populated resource type (theTarget.getResourceType() does not return a value)");
Validate.notBlank(
defaultString(theTarget.getIdPart()),
"theTarget must have a populated ID (theTarget.getIdPart() does not return a value)");
String wantRef = theTarget.toUnqualifiedVersionless().getValue();
RuntimeResourceDefinition sourceDef = myContext.getResourceDefinition(theSource);
if (theSource.getIdElement().hasIdPart()) {
if (wantRef.equals(
sourceDef.getName() + '/' + theSource.getIdElement().getIdPart())) {
return true;
}
}
class CompartmentOwnerVisitor implements ICompartmentOwnerVisitor {
private final String myWantRef;
public boolean isFound() {
return myFound;
}
private boolean myFound;
public CompartmentOwnerVisitor(String theWantRef) {
myWantRef = theWantRef;
}
@Override
public boolean consume(IIdType theCompartmentOwner) {
if (myWantRef.equals(
theCompartmentOwner.toUnqualifiedVersionless().getValue())) {
myFound = true;
}
return !myFound;
}
}
CompartmentOwnerVisitor consumer = new CompartmentOwnerVisitor(wantRef);
visitCompartmentOwnersForResource(theCompartmentName, theSource, theAdditionalCompartmentParamNames, consumer);
return consumer.isFound();
}
/**
* Returns the owners of the compartment in theSource
is in the compartment named theCompartmentName
.
*
* @param theCompartmentName The name of the compartment
* @param theSource The potential member of the compartment
* @param theAdditionalCompartmentParamNames If provided, search param names provided here will be considered as included in the given compartment for this comparison.
*/
@Nonnull
public List getCompartmentOwnersForResource(
String theCompartmentName, IBaseResource theSource, Set theAdditionalCompartmentParamNames) {
Validate.notBlank(theCompartmentName, "theCompartmentName must not be null or blank");
Validate.notNull(theSource, "theSource must not be null");
class CompartmentOwnerVisitor implements ICompartmentOwnerVisitor {
private final Set myOwnersAdded = new HashSet<>();
private final List myOwners = new ArrayList<>(2);
public List getOwners() {
return myOwners;
}
@Override
public boolean consume(IIdType theCompartmentOwner) {
if (myOwnersAdded.add(theCompartmentOwner.getValue())) {
myOwners.add(theCompartmentOwner);
}
return true;
}
}
CompartmentOwnerVisitor consumer = new CompartmentOwnerVisitor();
visitCompartmentOwnersForResource(theCompartmentName, theSource, theAdditionalCompartmentParamNames, consumer);
return consumer.getOwners();
}
private void visitCompartmentOwnersForResource(
String theCompartmentName,
IBaseResource theSource,
Set theAdditionalCompartmentParamNames,
ICompartmentOwnerVisitor theConsumer) {
Validate.notBlank(theCompartmentName, "theCompartmentName must not be null or blank");
Validate.notNull(theSource, "theSource must not be null");
RuntimeResourceDefinition sourceDef = myContext.getResourceDefinition(theSource);
List params = sourceDef.getSearchParamsForCompartmentName(theCompartmentName);
// If passed an additional set of searchparameter names, add them for comparison purposes.
if (theAdditionalCompartmentParamNames != null) {
List additionalParams = theAdditionalCompartmentParamNames.stream()
.map(sourceDef::getSearchParam)
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (params == null || params.isEmpty()) {
params = additionalParams;
} else {
List existingParams = params;
params = new ArrayList<>(existingParams.size() + additionalParams.size());
params.addAll(existingParams);
params.addAll(additionalParams);
}
}
for (RuntimeSearchParam nextParam : params) {
for (String nextPath : nextParam.getPathsSplit()) {
/*
* DSTU3 and before just defined compartments as being (e.g.) named
* Patient with a path like CarePlan.subject
*
* R4 uses a fancier format like CarePlan.subject.where(resolve() is Patient)
*
* The following Regex is a hack to make that efficient at runtime.
*/
String wantType = null;
Pattern pattern = COMPARTMENT_MATCHER_PATH;
Matcher matcher = pattern.matcher(nextPath);
if (matcher.matches()) {
nextPath = matcher.group(1);
wantType = matcher.group(2);
}
List values = getValues(theSource, nextPath, IBaseReference.class);
for (IBaseReference nextValue : values) {
IIdType nextTargetId = nextValue.getReferenceElement().toUnqualifiedVersionless();
/*
* If the reference isn't an explicit resource ID, but instead is just
* a resource object, we'll calculate its ID and treat the target
* as that.
*/
if (isBlank(nextTargetId.getValue()) && nextValue.getResource() != null) {
IBaseResource nextTarget = nextValue.getResource();
nextTargetId = nextTarget.getIdElement().toUnqualifiedVersionless();
if (!nextTargetId.hasResourceType()) {
String resourceType = myContext.getResourceType(nextTarget);
nextTargetId.setParts(null, resourceType, nextTargetId.getIdPart(), null);
}
}
if (isNotBlank(wantType)) {
String nextTargetIdResourceType = nextTargetId.getResourceType();
if (nextTargetIdResourceType == null || !nextTargetIdResourceType.equals(wantType)) {
continue;
}
}
if (isNotBlank(nextTargetId.getValue())) {
boolean shouldContinue = theConsumer.consume(nextTargetId);
if (!shouldContinue) {
return;
}
}
}
}
}
}
private void visit(
IBase theElement,
BaseRuntimeChildDefinition theChildDefinition,
BaseRuntimeElementDefinition> theDefinition,
IModelVisitor2 theCallback,
List theContainingElementPath,
List theChildDefinitionPath,
List> theElementDefinitionPath) {
if (theChildDefinition != null) {
theChildDefinitionPath.add(theChildDefinition);
}
theContainingElementPath.add(theElement);
theElementDefinitionPath.add(theDefinition);
boolean recurse = theCallback.acceptElement(
theElement,
Collections.unmodifiableList(theContainingElementPath),
Collections.unmodifiableList(theChildDefinitionPath),
Collections.unmodifiableList(theElementDefinitionPath));
if (recurse) {
/*
* Visit undeclared extensions
*/
if (theElement instanceof ISupportsUndeclaredExtensions) {
ISupportsUndeclaredExtensions containingElement = (ISupportsUndeclaredExtensions) theElement;
for (ExtensionDt nextExt : containingElement.getUndeclaredExtensions()) {
theContainingElementPath.add(nextExt);
theCallback.acceptUndeclaredExtension(
nextExt, theContainingElementPath, theChildDefinitionPath, theElementDefinitionPath);
theContainingElementPath.remove(theContainingElementPath.size() - 1);
}
}
/*
* Now visit the children of the given element
*/
switch (theDefinition.getChildType()) {
case ID_DATATYPE:
case PRIMITIVE_XHTML_HL7ORG:
case PRIMITIVE_XHTML:
case PRIMITIVE_DATATYPE:
// These are primitive types, so we don't need to visit their children
break;
case RESOURCE:
case RESOURCE_BLOCK:
case COMPOSITE_DATATYPE: {
BaseRuntimeElementCompositeDefinition> childDef =
(BaseRuntimeElementCompositeDefinition>) theDefinition;
for (BaseRuntimeChildDefinition nextChild : childDef.getChildrenAndExtension()) {
List extends IBase> values = nextChild.getAccessor().getValues(theElement);
if (values != null) {
for (IBase nextValue : values) {
if (nextValue == null) {
continue;
}
if (nextValue.isEmpty()) {
continue;
}
BaseRuntimeElementDefinition> childElementDef;
Class extends IBase> valueType = nextValue.getClass();
childElementDef = nextChild.getChildElementDefinitionByDatatype(valueType);
while (childElementDef == null && IBase.class.isAssignableFrom(valueType)) {
childElementDef = nextChild.getChildElementDefinitionByDatatype(valueType);
valueType = (Class extends IBase>) valueType.getSuperclass();
}
Class extends IBase> typeClass = nextValue.getClass();
while (childElementDef == null && IBase.class.isAssignableFrom(typeClass)) {
//noinspection unchecked
typeClass = (Class extends IBase>) typeClass.getSuperclass();
childElementDef = nextChild.getChildElementDefinitionByDatatype(typeClass);
}
Validate.notNull(
childElementDef,
"Found value of type[%s] which is not valid for field[%s] in %s",
nextValue.getClass(),
nextChild.getElementName(),
childDef.getName());
visit(
nextValue,
nextChild,
childElementDef,
theCallback,
theContainingElementPath,
theChildDefinitionPath,
theElementDefinitionPath);
}
}
}
break;
}
case CONTAINED_RESOURCES: {
BaseContainedDt value = (BaseContainedDt) theElement;
for (IResource next : value.getContainedResources()) {
BaseRuntimeElementCompositeDefinition> def = myContext.getResourceDefinition(next);
visit(
next,
null,
def,
theCallback,
theContainingElementPath,
theChildDefinitionPath,
theElementDefinitionPath);
}
break;
}
case EXTENSION_DECLARED:
case UNDECL_EXT: {
throw new IllegalStateException(
Msg.code(1793) + "state should not happen: " + theDefinition.getChildType());
}
case CONTAINED_RESOURCE_LIST: {
if (theElement != null) {
BaseRuntimeElementDefinition> def = myContext.getElementDefinition(theElement.getClass());
visit(
theElement,
null,
def,
theCallback,
theContainingElementPath,
theChildDefinitionPath,
theElementDefinitionPath);
}
break;
}
}
}
if (theChildDefinition != null) {
theChildDefinitionPath.remove(theChildDefinitionPath.size() - 1);
}
theContainingElementPath.remove(theContainingElementPath.size() - 1);
theElementDefinitionPath.remove(theElementDefinitionPath.size() - 1);
}
/**
* Visit all elements in a given resource
*
*
* 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.
* {@link BaseResourceReferenceDt#getResource()}) or embedded resources (e.g. Bundle.entry.resource)
*
*
* @param theResource The resource to visit
* @param theVisitor The visitor
*/
public void visit(IBaseResource theResource, IModelVisitor theVisitor) {
BaseRuntimeElementCompositeDefinition> def = myContext.getResourceDefinition(theResource);
visit(newMap(), theResource, theResource, null, null, def, theVisitor);
}
public Map