ca.uhn.fhir.context.ModelScanner Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of hapi-fhir-base Show documentation
Show all versions of hapi-fhir-base Show documentation
Builds the hapi fhir base. Requires Common lang, commons-text, and guava be built first.
/*
* #%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.context;
import ca.uhn.fhir.context.RuntimeSearchParam.RuntimeSearchParamStatusEnum;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.model.api.BaseIdentifiableElement;
import ca.uhn.fhir.model.api.ExtensionDt;
import ca.uhn.fhir.model.api.IDatatype;
import ca.uhn.fhir.model.api.IElement;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.IResourceBlock;
import ca.uhn.fhir.model.api.IValueSetEnumBinder;
import ca.uhn.fhir.model.api.annotation.Block;
import ca.uhn.fhir.model.api.annotation.Child;
import ca.uhn.fhir.model.api.annotation.Compartment;
import ca.uhn.fhir.model.api.annotation.DatatypeDef;
import ca.uhn.fhir.model.api.annotation.ResourceDef;
import ca.uhn.fhir.model.api.annotation.SearchParamDefinition;
import ca.uhn.fhir.model.primitive.BoundCodeDt;
import ca.uhn.fhir.model.primitive.XhtmlDt;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.util.ReflectionUtil;
import jakarta.annotation.Nonnull;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseBackboneElement;
import org.hl7.fhir.instance.model.api.IBaseDatatype;
import org.hl7.fhir.instance.model.api.IBaseDatatypeElement;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IBaseXhtml;
import org.hl7.fhir.instance.model.api.ICompositeType;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
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.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import static org.apache.commons.lang3.StringUtils.isBlank;
class ModelScanner {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ModelScanner.class);
private Map, BaseRuntimeElementDefinition>> myClassToElementDefinitions = new HashMap<>();
private FhirContext myContext;
private Map myIdToResourceDefinition = new HashMap<>();
private Map> myNameToElementDefinitions = new HashMap<>();
private Map myNameToResourceDefinitions = new HashMap<>();
private Map> myNameToResourceType = new HashMap<>();
private RuntimeChildUndeclaredExtensionDefinition myRuntimeChildUndeclaredExtensionDefinition;
private Set> myScanAlso = new HashSet<>();
private FhirVersionEnum myVersion;
private Set> myVersionTypes;
ModelScanner(
FhirContext theContext,
FhirVersionEnum theVersion,
Map, BaseRuntimeElementDefinition>> theExistingDefinitions,
@Nonnull Collection> theResourceTypes)
throws ConfigurationException {
myContext = theContext;
myVersion = theVersion;
Set> toScan = new HashSet<>(theResourceTypes);
init(theExistingDefinitions, toScan);
}
Map, BaseRuntimeElementDefinition>> getClassToElementDefinitions() {
return myClassToElementDefinitions;
}
Map getIdToResourceDefinition() {
return myIdToResourceDefinition;
}
Map> getNameToElementDefinitions() {
return myNameToElementDefinitions;
}
Map getNameToResourceDefinition() {
return myNameToResourceDefinitions;
}
Map> getNameToResourceType() {
return myNameToResourceType;
}
RuntimeChildUndeclaredExtensionDefinition getRuntimeChildUndeclaredExtensionDefinition() {
return myRuntimeChildUndeclaredExtensionDefinition;
}
private void init(
Map, BaseRuntimeElementDefinition>> theExistingDefinitions,
Set> theTypesToScan) {
if (theExistingDefinitions != null) {
myClassToElementDefinitions.putAll(theExistingDefinitions);
}
int startSize = myClassToElementDefinitions.size();
long start = System.currentTimeMillis();
Map> resourceTypes = myNameToResourceType;
Set> typesToScan = theTypesToScan;
myVersionTypes = scanVersionPropertyFile(typesToScan, resourceTypes, myVersion, myClassToElementDefinitions);
do {
for (Class extends IBase> nextClass : typesToScan) {
scan(nextClass);
}
myScanAlso.removeIf(theClass -> myClassToElementDefinitions.containsKey(theClass));
typesToScan.clear();
typesToScan.addAll(myScanAlso);
myScanAlso.clear();
} while (!typesToScan.isEmpty());
for (Entry, BaseRuntimeElementDefinition>> nextEntry :
myClassToElementDefinitions.entrySet()) {
if (theExistingDefinitions != null && theExistingDefinitions.containsKey(nextEntry.getKey())) {
continue;
}
BaseRuntimeElementDefinition> next = nextEntry.getValue();
boolean deferredSeal = false;
if (myContext.getPerformanceOptions().contains(PerformanceOptionsEnum.DEFERRED_MODEL_SCANNING)) {
if (next instanceof BaseRuntimeElementCompositeDefinition) {
deferredSeal = true;
}
}
if (!deferredSeal) {
next.sealAndInitialize(myContext, myClassToElementDefinitions);
}
}
myRuntimeChildUndeclaredExtensionDefinition = new RuntimeChildUndeclaredExtensionDefinition();
myRuntimeChildUndeclaredExtensionDefinition.sealAndInitialize(myContext, myClassToElementDefinitions);
long time = System.currentTimeMillis() - start;
int size = myClassToElementDefinitions.size() - startSize;
ourLog.debug("Done scanning FHIR library, found {} model entries in {}ms", size, time);
}
private boolean isStandardType(Class extends IBase> theClass) {
return myVersionTypes.contains(theClass);
}
void scan(Class extends IBase> theClass) throws ConfigurationException {
BaseRuntimeElementDefinition> existingDef = myClassToElementDefinitions.get(theClass);
if (existingDef != null) {
return;
}
ResourceDef resourceDefinition = pullAnnotation(theClass, ResourceDef.class);
if (resourceDefinition != null) {
if (!IBaseResource.class.isAssignableFrom(theClass)) {
throw new ConfigurationException(Msg.code(1714) + "Resource type contains a @"
+ ResourceDef.class.getSimpleName() + " annotation but does not implement "
+ IResource.class.getCanonicalName() + ": " + theClass.getCanonicalName());
}
@SuppressWarnings("unchecked")
Class extends IBaseResource> resClass = (Class extends IBaseResource>) theClass;
scanResource(resClass, resourceDefinition);
return;
}
DatatypeDef datatypeDefinition = pullAnnotation(theClass, DatatypeDef.class);
if (datatypeDefinition != null) {
if (ICompositeType.class.isAssignableFrom(theClass)) {
@SuppressWarnings("unchecked")
Class extends ICompositeType> resClass = (Class extends ICompositeType>) theClass;
scanCompositeDatatype(resClass, datatypeDefinition);
} else if (IPrimitiveType.class.isAssignableFrom(theClass)) {
@SuppressWarnings({"unchecked"})
Class extends IPrimitiveType>> resClass = (Class extends IPrimitiveType>>) theClass;
scanPrimitiveDatatype(resClass, datatypeDefinition);
}
return;
}
Block blockDefinition = pullAnnotation(theClass, Block.class);
if (blockDefinition != null) {
if (IResourceBlock.class.isAssignableFrom(theClass)
|| IBaseBackboneElement.class.isAssignableFrom(theClass)
|| IBaseDatatypeElement.class.isAssignableFrom(theClass)) {
scanBlock(theClass);
} else {
throw new ConfigurationException(Msg.code(1715) + "Type contains a @" + Block.class.getSimpleName()
+ " annotation but does not implement " + IResourceBlock.class.getCanonicalName() + ": "
+ theClass.getCanonicalName());
}
}
if (blockDefinition == null) {
if (theClass.getSimpleName().equals("XhtmlType")) {
return;
}
throw new ConfigurationException(Msg.code(1716) + "Resource class[" + theClass.getName()
+ "] does not contain any valid HAPI-FHIR annotations");
}
}
private void scanBlock(Class extends IBase> theClass) {
ourLog.debug("Scanning resource block class: {}", theClass.getName());
String blockName = theClass.getSimpleName();
// Just in case someone messes up when upgrading from DSTU2
if (myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) {
if (BaseIdentifiableElement.class.isAssignableFrom(theClass)) {
throw new ConfigurationException(Msg.code(1717) + "@Block class for version "
+ myContext.getVersion().getVersion().name() + " should not extend "
+ BaseIdentifiableElement.class.getSimpleName() + ": " + theClass.getName());
}
}
RuntimeResourceBlockDefinition blockDef = new RuntimeResourceBlockDefinition(
blockName, theClass, isStandardType(theClass), myContext, myClassToElementDefinitions);
blockDef.populateScanAlso(myScanAlso);
myClassToElementDefinitions.put(theClass, blockDef);
}
private void scanCompositeDatatype(Class extends ICompositeType> theClass, DatatypeDef theDatatypeDefinition) {
ourLog.debug("Scanning datatype class: {}", theClass.getName());
RuntimeCompositeDatatypeDefinition elementDef;
if (theClass.equals(ExtensionDt.class)) {
elementDef = new RuntimeExtensionDtDefinition(
theDatatypeDefinition, theClass, true, myContext, myClassToElementDefinitions);
// } else if (IBaseMetaType.class.isAssignableFrom(theClass)) {
// resourceDef = new RuntimeMetaDefinition(theDatatypeDefinition, theClass, isStandardType(theClass));
} else {
elementDef = new RuntimeCompositeDatatypeDefinition(
theDatatypeDefinition, theClass, isStandardType(theClass), myContext, myClassToElementDefinitions);
}
myClassToElementDefinitions.put(theClass, elementDef);
myNameToElementDefinitions.put(elementDef.getName().toLowerCase(), elementDef);
/*
* See #423:
* If the type contains a field that has a custom type, we want to make
* sure that this type gets scanned as well
*/
elementDef.populateScanAlso(myScanAlso);
}
private String scanPrimitiveDatatype(
Class extends IPrimitiveType>> theClass, DatatypeDef theDatatypeDefinition) {
ourLog.debug("Scanning resource class: {}", theClass.getName());
String resourceName = theDatatypeDefinition.name();
if (isBlank(resourceName)) {
throw new ConfigurationException(Msg.code(1718) + "Resource type @" + ResourceDef.class.getSimpleName()
+ " annotation contains no resource name: " + theClass.getCanonicalName());
}
BaseRuntimeElementDefinition> elementDef;
if (theClass.equals(XhtmlDt.class)) {
@SuppressWarnings("unchecked")
Class clazz = (Class) theClass;
elementDef = new RuntimePrimitiveDatatypeNarrativeDefinition(resourceName, clazz, isStandardType(clazz));
} else if (IBaseXhtml.class.isAssignableFrom(theClass)) {
@SuppressWarnings("unchecked")
Class extends IBaseXhtml> clazz = (Class extends IBaseXhtml>) theClass;
elementDef = new RuntimePrimitiveDatatypeXhtmlHl7OrgDefinition(resourceName, clazz, isStandardType(clazz));
} else if (IIdType.class.isAssignableFrom(theClass)) {
elementDef = new RuntimeIdDatatypeDefinition(theDatatypeDefinition, theClass, isStandardType(theClass));
} else {
elementDef =
new RuntimePrimitiveDatatypeDefinition(theDatatypeDefinition, theClass, isStandardType(theClass));
}
myClassToElementDefinitions.put(theClass, elementDef);
if (!theDatatypeDefinition.isSpecialization()) {
if (myVersion.isRi() && IDatatype.class.isAssignableFrom(theClass)) {
ourLog.debug("Not adding non RI type {} to RI context", theClass);
} else if (!myVersion.isRi() && !IDatatype.class.isAssignableFrom(theClass)) {
ourLog.debug("Not adding RI type {} to non RI context", theClass);
} else {
myNameToElementDefinitions.put(resourceName, elementDef);
}
}
return resourceName;
}
private String scanResource(Class extends IBaseResource> theClass, ResourceDef resourceDefinition) {
ourLog.debug("Scanning resource class: {}", theClass.getName());
boolean primaryNameProvider = true;
String resourceName = resourceDefinition.name();
if (isBlank(resourceName)) {
Class> parent = theClass.getSuperclass();
primaryNameProvider = false;
while (parent.equals(Object.class) == false && isBlank(resourceName)) {
ResourceDef nextDef = pullAnnotation(parent, ResourceDef.class);
if (nextDef != null) {
resourceName = nextDef.name();
}
parent = parent.getSuperclass();
}
if (isBlank(resourceName)) {
throw new ConfigurationException(Msg.code(1719) + "Resource type @" + ResourceDef.class.getSimpleName()
+ " annotation contains no resource name(): " + theClass.getCanonicalName()
+ " - This is only allowed for types that extend other resource types ");
}
}
String resourceNameLowerCase = resourceName.toLowerCase();
Class extends IBaseResource> builtInType = myNameToResourceType.get(resourceNameLowerCase);
boolean standardType = builtInType != null && builtInType.equals(theClass) == true;
if (primaryNameProvider) {
if (builtInType != null && builtInType.equals(theClass) == false) {
primaryNameProvider = false;
}
}
String resourceId = resourceDefinition.id();
if (!isBlank(resourceId)) {
if (myIdToResourceDefinition.containsKey(resourceId)) {
throw new ConfigurationException(Msg.code(1720) + "The following resource types have the same ID of '"
+ resourceId + "' - " + theClass.getCanonicalName() + " and "
+ myIdToResourceDefinition
.get(resourceId)
.getImplementingClass()
.getCanonicalName());
}
}
RuntimeResourceDefinition resourceDef = new RuntimeResourceDefinition(
myContext, resourceName, theClass, resourceDefinition, standardType, myClassToElementDefinitions);
myClassToElementDefinitions.put(theClass, resourceDef);
if (primaryNameProvider) {
if (resourceDef.getStructureVersion() == myVersion) {
myNameToResourceDefinitions.put(resourceNameLowerCase, resourceDef);
}
// TODO: Address this when core lib version is bumped
if (resourceDef.getStructureVersion() == FhirVersionEnum.R5 && myVersion == FhirVersionEnum.R4B) {
myNameToResourceDefinitions.put(resourceNameLowerCase, resourceDef);
}
}
myIdToResourceDefinition.put(resourceId, resourceDef);
scanResourceForSearchParams(theClass, resourceDef);
/*
* See #423:
* If the type contains a field that has a custom type, we want to make
* sure that this type gets scanned as well
*/
resourceDef.populateScanAlso(myScanAlso);
return resourceName;
}
private void scanResourceForSearchParams(
Class extends IBaseResource> theClass, RuntimeResourceDefinition theResourceDef) {
Map nameToParam = new HashMap<>();
Map compositeFields = new LinkedHashMap<>();
/*
* Make sure we pick up fields in interfaces too.. This ensures that we
* grab the _id field which generally gets picked up via interface
*/
Set fields = new HashSet<>(Arrays.asList(theClass.getFields()));
Class> nextClass = theClass;
do {
for (Class> nextInterface : nextClass.getInterfaces()) {
fields.addAll(Arrays.asList(nextInterface.getFields()));
}
nextClass = nextClass.getSuperclass();
} while (nextClass.equals(Object.class) == false);
/*
* Now scan the fields for search params
*/
for (Field nextField : fields) {
SearchParamDefinition searchParam = pullAnnotation(nextField, SearchParamDefinition.class);
if (searchParam != null) {
RestSearchParameterTypeEnum paramType =
RestSearchParameterTypeEnum.forCode(searchParam.type().toLowerCase());
if (paramType == null) {
throw new ConfigurationException(Msg.code(1721) + "Search param " + searchParam.name()
+ " has an invalid type: " + searchParam.type());
}
Set providesMembershipInCompartments;
providesMembershipInCompartments = new HashSet<>();
for (Compartment next : searchParam.providesMembershipIn()) {
if (paramType != RestSearchParameterTypeEnum.REFERENCE) {
StringBuilder b = new StringBuilder();
b.append("Search param ");
b.append(searchParam.name());
b.append(" on resource type ");
b.append(theClass.getName());
b.append(" provides compartment membership but is not of type 'reference'");
ourLog.warn(b.toString());
continue;
}
String name = next.name();
// As of 2021-12-28 the R5 structures incorrectly have this prefix
if (name.startsWith("Base FHIR compartment definition for ")) {
name = name.substring("Base FHIR compartment definition for ".length());
}
providesMembershipInCompartments.add(name);
}
List components = null;
if (paramType == RestSearchParameterTypeEnum.COMPOSITE) {
components = new ArrayList<>();
for (String name : searchParam.compositeOf()) {
String ref = toCanonicalSearchParameterUri(theResourceDef, name);
components.add(new RuntimeSearchParam.Component(null, ref));
}
}
Collection base = Collections.singletonList(theResourceDef.getName());
String url = null;
if (theResourceDef.isStandardType()) {
String name = searchParam.name();
url = toCanonicalSearchParameterUri(theResourceDef, name);
}
RuntimeSearchParam param = new RuntimeSearchParam(
null,
url,
searchParam.name(),
searchParam.description(),
searchParam.path(),
paramType,
providesMembershipInCompartments,
toTargetList(searchParam.target()),
RuntimeSearchParamStatusEnum.ACTIVE,
null,
components,
base);
theResourceDef.addSearchParam(param);
nameToParam.put(param.getName(), param);
}
}
}
private String toCanonicalSearchParameterUri(RuntimeResourceDefinition theResourceDef, String theName) {
return "http://hl7.org/fhir/SearchParameter/" + theResourceDef.getName() + "-" + theName;
}
private Set toTargetList(Class extends IBaseResource>[] theTarget) {
HashSet retVal = new HashSet<>();
for (Class extends IBaseResource> nextType : theTarget) {
ResourceDef resourceDef = nextType.getAnnotation(ResourceDef.class);
if (resourceDef != null) {
retVal.add(resourceDef.name());
}
}
return retVal;
}
static Class> determineElementType(Field next) {
Class> nextElementType = next.getType();
if (List.class.equals(nextElementType)) {
nextElementType = ReflectionUtil.getGenericCollectionTypeOfField(next);
} else if (Collection.class.isAssignableFrom(nextElementType)) {
throw new ConfigurationException(Msg.code(1722) + "Field '" + next.getName() + "' in type '"
+ nextElementType.getCanonicalName()
+ "' is a Collection - Only java.util.List curently supported");
}
return nextElementType;
}
@SuppressWarnings("unchecked")
static IValueSetEnumBinder> getBoundCodeBinder(Field theNext) {
Class> bound = getGenericCollectionTypeOfCodedField(theNext);
if (bound == null) {
throw new ConfigurationException(Msg.code(1723) + "Field '" + theNext + "' has no parameter for "
+ BoundCodeDt.class.getSimpleName() + " to determine enum type");
}
String fieldName = "VALUESET_BINDER";
try {
Field bindingField = bound.getField(fieldName);
return (IValueSetEnumBinder>) bindingField.get(null);
} catch (Exception e) {
throw new ConfigurationException(
Msg.code(1724) + "Field '" + theNext + "' has type parameter " + bound.getCanonicalName()
+ " but this class has no valueset binding field (must have a field called " + fieldName
+ ")",
e);
}
}
/**
* There are two implementations of all of the annotations (e.g. {@link Child} since the HL7.org ones will eventually replace the HAPI
* ones. Annotations can't extend each other or implement interfaces or anything like that, so rather than duplicate all of the annotation processing code this method just creates an interface
* Proxy to simulate the HAPI annotations if the HL7.org ones are found instead.
*/
static T pullAnnotation(AnnotatedElement theTarget, Class theAnnotationType) {
T retVal = theTarget.getAnnotation(theAnnotationType);
return retVal;
}
static Class extends Enum>> determineEnumTypeForBoundField(Field next) {
@SuppressWarnings("unchecked")
Class extends Enum>> enumType =
(Class extends Enum>>) ReflectionUtil.getGenericCollectionTypeOfFieldWithSecondOrderForList(next);
return enumType;
}
private static Class> getGenericCollectionTypeOfCodedField(Field next) {
Class> type;
ParameterizedType collectionType = (ParameterizedType) next.getGenericType();
Type firstArg = collectionType.getActualTypeArguments()[0];
if (ParameterizedType.class.isAssignableFrom(firstArg.getClass())) {
ParameterizedType pt = ((ParameterizedType) firstArg);
firstArg = pt.getActualTypeArguments()[0];
type = (Class>) firstArg;
} else {
type = (Class>) firstArg;
}
return type;
}
static Set> scanVersionPropertyFile(
Set> theDatatypes,
Map> theResourceTypes,
FhirVersionEnum theVersion,
Map, BaseRuntimeElementDefinition>> theExistingElementDefinitions) {
Set> retVal = new HashSet<>();
try (InputStream str = theVersion.getVersionImplementation().getFhirVersionPropertiesFile()) {
Properties prop = new Properties();
prop.load(str);
for (Entry
© 2015 - 2024 Weber Informatics LLC | Privacy Policy