All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ca.uhn.fhir.context.ModelScanner Maven / Gradle / Ivy

There is a newer version: 7.6.1
Show newest version
package ca.uhn.fhir.context;

/*
 * #%L
 * HAPI FHIR - Core Library
 * %%
 * Copyright (C) 2014 - 2016 University Health Network
 * %%
 * 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%
 */
import static org.apache.commons.lang3.StringUtils.isBlank;

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.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
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 org.apache.commons.io.IOUtils;
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 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.method.RestSearchParameterTypeEnum;
import ca.uhn.fhir.util.ReflectionUtil;

class ModelScanner {
	private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ModelScanner.class);

	private Map, BaseRuntimeElementDefinition> myClassToElementDefinitions = new HashMap, BaseRuntimeElementDefinition>();
	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,
			Collection> theResourceTypes) throws ConfigurationException {
		myContext = theContext;
		myVersion = theVersion;
		Set> toScan;
		if (theResourceTypes != null) {
			toScan = new HashSet>(theResourceTypes);
		} else {
			toScan = new HashSet>();
		}
		init(theExistingDefinitions, toScan);
	}

	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("Field '" + next.getName() + "' in type '" + next.getClass().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("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("Field '" + theNext + "' has type parameter " + bound.getCanonicalName() + " but this class has no valueset binding field (must have a field called " + fieldName + ")", e);
		}
	}

	public Map, BaseRuntimeElementDefinition> getClassToElementDefinitions() {
		return myClassToElementDefinitions;
	}

	public Map getIdToResourceDefinition() {
		return myIdToResourceDefinition;
	}

	public Map> getNameToElementDefinitions() {
		return myNameToElementDefinitions;
	}

	public Map getNameToResourceDefinition() {
		return myNameToResourceDefinitions;
	}

	public Map getNameToResourceDefinitions() {
		return (myNameToResourceDefinitions);
	}

	public Map> getNameToResourceType() {
		return myNameToResourceType;
	}

	public 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 nextClass : typesToScan) {
				scan(nextClass);
			}
			for (Iterator> iter = myScanAlso.iterator(); iter.hasNext();) {
				if (myClassToElementDefinitions.containsKey(iter.next())) {
					iter.remove();
				}
			}
			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 theClass) {
		boolean retVal = myVersionTypes.contains(theClass);
		return retVal;
	}

	/**
	 * There are two implementations of all of the annotations (e.g. {@link Child} and {@link org.hl7.fhir.instance.model.annotations.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;
	}

	private void scan(Class 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(
						"Resource type contains a @" + ResourceDef.class.getSimpleName() + " annotation but does not implement " + IResource.class.getCanonicalName() + ": " + theClass.getCanonicalName());
			}
			@SuppressWarnings("unchecked")
			Class resClass = (Class) theClass;
			scanResource(resClass, resourceDefinition);
			return;
		}

		DatatypeDef datatypeDefinition = pullAnnotation(theClass, DatatypeDef.class);
		if (datatypeDefinition != null) {
			if (ICompositeType.class.isAssignableFrom(theClass)) {
				@SuppressWarnings("unchecked")
				Class resClass = (Class) theClass;
				scanCompositeDatatype(resClass, datatypeDefinition);
			} else if (IPrimitiveType.class.isAssignableFrom(theClass)) {
				@SuppressWarnings({ "unchecked" })
				Class> resClass = (Class>) 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(
						"Type contains a @" + Block.class.getSimpleName() + " annotation but does not implement " + IResourceBlock.class.getCanonicalName() + ": " + theClass.getCanonicalName());
			}
		}

		if (blockDefinition == null && datatypeDefinition == null && resourceDefinition == null) {
			throw new ConfigurationException("Resource class[" + theClass.getName() + "] does not contain any valid HAPI-FHIR annotations");
		}
	}

	private void scanBlock(Class theClass) {
		ourLog.debug("Scanning resource block class: {}", theClass.getName());

		String resourceName = theClass.getCanonicalName();
		if (isBlank(resourceName)) {
			throw new ConfigurationException("Block type @" + Block.class.getSimpleName() + " annotation contains no name: " + theClass.getCanonicalName());
		}

		RuntimeResourceBlockDefinition blockDef = new RuntimeResourceBlockDefinition(resourceName, theClass, isStandardType(theClass), myContext, myClassToElementDefinitions);
		myClassToElementDefinitions.put(theClass, blockDef);
	}

	private void scanCompositeDatatype(Class 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);
	}



	static Class> determineEnumTypeForBoundField(Field next) {
		@SuppressWarnings("unchecked")
		Class> enumType = (Class>) ReflectionUtil.getGenericCollectionTypeOfFieldWithSecondOrderForList(next);
		return enumType;
	}

	private String scanPrimitiveDatatype(Class> theClass, DatatypeDef theDatatypeDefinition) {
		ourLog.debug("Scanning resource class: {}", theClass.getName());

		String resourceName = theDatatypeDefinition.name();
		if (isBlank(resourceName)) {
			throw new ConfigurationException("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 clazz = (Class) 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 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("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 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("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);
			}
		}

		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 theClass, RuntimeResourceDefinition theResourceDef) {

		Map nameToParam = new HashMap();
		Map compositeFields = new LinkedHashMap();

		for (Field nextField : theClass.getFields()) {
			SearchParamDefinition searchParam = pullAnnotation(nextField, SearchParamDefinition.class);
			if (searchParam != null) {
				RestSearchParameterTypeEnum paramType = RestSearchParameterTypeEnum.forCode(searchParam.type().toLowerCase());
				if (paramType == null) {
					throw new ConfigurationException("Search param " + searchParam.name() + " has an invalid type: " + searchParam.type());
				}
				Set providesMembershipInCompartments = null;
				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;
//						throw new ConfigurationException(b.toString());
					}
					providesMembershipInCompartments.add(next.name());
				}
				
				if (paramType == RestSearchParameterTypeEnum.COMPOSITE) {
					compositeFields.put(nextField, searchParam);
					continue;
				}


				RuntimeSearchParam param = new RuntimeSearchParam(searchParam.name(), searchParam.description(), searchParam.path(), paramType, providesMembershipInCompartments, toTargetList(searchParam.target()));
				theResourceDef.addSearchParam(param);
				nameToParam.put(param.getName(), param);
			}
		}

		for (Entry nextEntry : compositeFields.entrySet()) {
			SearchParamDefinition searchParam = nextEntry.getValue();

			List compositeOf = new ArrayList();
			for (String nextName : searchParam.compositeOf()) {
				RuntimeSearchParam param = nameToParam.get(nextName);
				if (param == null) {
					ourLog.warn("Search parameter {}.{} declares that it is a composite with compositeOf value '{}' but that is not a valid parametr name itself. Valid values are: {}",
							new Object[] { theResourceDef.getName(), searchParam.name(), nextName, nameToParam.keySet() });
					continue;
				}
				compositeOf.add(param);
			}

			RuntimeSearchParam param = new RuntimeSearchParam(searchParam.name(), searchParam.description(), searchParam.path(), RestSearchParameterTypeEnum.COMPOSITE, compositeOf, null, toTargetList(searchParam.target()));
			theResourceDef.addSearchParam(param);
		}
	}

	private Set toTargetList(Class[] theTarget) {
		HashSet retVal = new HashSet();
		
		for (Class nextType : theTarget) {
			ResourceDef resourceDef = nextType.getAnnotation(ResourceDef.class);
			if (resourceDef != null) {
				retVal.add(resourceDef.name());
			}
		}
		
		return retVal;
	}

	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>();

		InputStream str = theVersion.getVersionImplementation().getFhirVersionPropertiesFile();
		Properties prop = new Properties();
		try {
			prop.load(str);
			for (Entry nextEntry : prop.entrySet()) {
				String nextKey = nextEntry.getKey().toString();
				String nextValue = nextEntry.getValue().toString();

				if (nextKey.startsWith("datatype.")) {
					if (theDatatypes != null) {
						try {
							// Datatypes

							@SuppressWarnings("unchecked")
							Class dtType = (Class) Class.forName(nextValue);
							if (theExistingElementDefinitions.containsKey(dtType)) {
								continue;
							}
							retVal.add(dtType);

							if (IElement.class.isAssignableFrom(dtType)) {
								@SuppressWarnings("unchecked")
								Class nextClass = (Class) dtType;
								theDatatypes.add(nextClass);
							} else if (IBaseDatatype.class.isAssignableFrom(dtType)) {
								@SuppressWarnings("unchecked")
								Class nextClass = (Class) dtType;
								theDatatypes.add(nextClass);
							} else {
								ourLog.warn("Class is not assignable from " + IElement.class.getSimpleName() + " or " + IBaseDatatype.class.getSimpleName() + ": " + nextValue);
								continue;
							}

						} catch (ClassNotFoundException e) {
							throw new ConfigurationException("Unknown class[" + nextValue + "] for data type definition: " + nextKey.substring("datatype.".length()), e);
						}
					}
				} else if (nextKey.startsWith("resource.")) {
					// Resources
					String resName = nextKey.substring("resource.".length()).toLowerCase();
					try {
						@SuppressWarnings("unchecked")
						Class nextClass = (Class) Class.forName(nextValue);
						if (theExistingElementDefinitions.containsKey(nextClass)) {
							continue;
						}
						if (!IBaseResource.class.isAssignableFrom(nextClass)) {
							throw new ConfigurationException("Class is not assignable from " + IBaseResource.class.getSimpleName() + ": " + nextValue);
						}

						theResourceTypes.put(resName, nextClass);
					} catch (ClassNotFoundException e) {
						throw new ConfigurationException("Unknown class[" + nextValue + "] for resource definition: " + nextKey.substring("resource.".length()), e);
					}
				} else {
					throw new ConfigurationException("Unexpected property in version property file: " + nextKey + "=" + nextValue);
				}
			}
		} catch (IOException e) {
			throw new ConfigurationException("Failed to load model property file from classpath: " + "/ca/uhn/fhir/model/dstu/model.properties");
		} finally {
			IOUtils.closeQuietly(str);
		}

		return retVal;
	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy