package ca.uhn.fhir.util; import static org.apache.commons.lang3.StringUtils.defaultString; /* * #%L * HAPI FHIR - Core Library * %% * Copyright (C) 2014 - 2017 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 java.util.*; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.*; import ca.uhn.fhir.context.*; import ca.uhn.fhir.context.BaseRuntimeElementDefinition.ChildTypeEnum; import ca.uhn.fhir.model.api.ExtensionDt; 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.StringDt; import ca.uhn.fhir.parser.DataFormatException; public class FhirTerser { private FhirContext myContext; public FhirTerser(FhirContext theContext) { super(); myContext = theContext; } private List<String> addNameToList(List<String> theCurrentList, BaseRuntimeChildDefinition theChildDefinition) { if (theChildDefinition == null) return null; if (theCurrentList == null || theCurrentList.isEmpty()) return new ArrayList<String>(Arrays.asList(theChildDefinition.getElementName())); List<String> newList = new ArrayList<String>(theCurrentList); newList.add(theChildDefinition.getElementName()); return newList; } /** * 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) */ public void cloneInto(IBase theSource, IBase theTarget, boolean theIgnoreMissingFields) { Validate.notNull(theSource, "theSource must not be null"); Validate.notNull(theTarget, "theTarget must not be null"); if (theSource instanceof IPrimitiveType<?>) { if (theTarget instanceof IPrimitiveType<?>) { ((IPrimitiveType<?>)theTarget).setValueAsString(((IPrimitiveType<?>)theSource).getValueAsString()); return; } if (theIgnoreMissingFields) { return; } throw new DataFormatException("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<BaseRuntimeChildDefinition> children = sourceDef.getChildren(); if (sourceDef instanceof RuntimeExtensionDtDefinition) { children = ((RuntimeExtensionDtDefinition)sourceDef).getChildrenIncludingUrl(); } for (BaseRuntimeChildDefinition nextChild : children) { for (IBase nextValue : nextChild.getAccessor().getValues(theSource)) { String elementName = nextChild.getChildNameByDatatype(nextValue.getClass()); BaseRuntimeChildDefinition targetChild = targetDef.getChildByName(elementName); if (targetChild == null) { if (theIgnoreMissingFields) { continue; } throw new DataFormatException("Type " + theTarget.getClass().getName() + " does not have a child with name " + elementName); } BaseRuntimeElementDefinition<?> childDef = targetChild.getChildByName(elementName); IBase target = childDef.newInstance(); targetChild.getMutator().addValue(theTarget, target); cloneInto(nextValue, target, theIgnoreMissingFields); } } } /** * 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. * <p> * 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. * </p> * <p> * 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) * </p> * * @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 <T extends IBase> List<T> getAllPopulatedChildElementsOfType(IBaseResource theResource, final Class<T> theType) { final ArrayList<T> retVal = new ArrayList<T>(); BaseRuntimeElementCompositeDefinition<?> def = myContext.getResourceDefinition(theResource); visit(new IdentityHashMap<Object, Object>(), theResource, theResource, null, null, def, new IModelVisitor() { @SuppressWarnings("unchecked") @Override public void acceptElement(IBaseResource theOuterResource, IBase theElement, List<String> thePathToElement, BaseRuntimeChildDefinition theChildDefinition, BaseRuntimeElementDefinition<?> theDefinition) { if (theElement == null || theElement.isEmpty()) { return; } if (theType.isAssignableFrom(theElement.getClass())) { retVal.add((T) theElement); } } }); return retVal; } public List<ResourceReferenceInfo> getAllResourceReferences(final IBaseResource theResource) { final ArrayList<ResourceReferenceInfo> retVal = new ArrayList<ResourceReferenceInfo>(); BaseRuntimeElementCompositeDefinition<?> def = myContext.getResourceDefinition(theResource); visit(new IdentityHashMap<Object, Object>(), theResource, theResource, null, null, def, new IModelVisitor() { @Override public void acceptElement(IBaseResource theOuterResource, IBase theElement, List<String> thePathToElement, BaseRuntimeChildDefinition theChildDefinition, BaseRuntimeElementDefinition<?> theDefinition) { if (theElement == null || theElement.isEmpty()) { return; } if (IBaseReference.class.isAssignableFrom(theElement.getClass())) { retVal.add(new ResourceReferenceInfo(myContext, theOuterResource, thePathToElement, (IBaseReference) theElement)); } } }); return retVal; } private BaseRuntimeChildDefinition getDefinition(BaseRuntimeElementCompositeDefinition<?> theCurrentDef, List<String> 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); BaseRuntimeElementCompositeDefinition<?> currentDef = def; List<String> parts = Arrays.asList(thePath.split("\\.")); List<String> subList = parts.subList(1, parts.size()); if (subList.size() < 1) { throw new ConfigurationException("Invalid path: " + thePath); } return getDefinition(currentDef, subList); } public Object getSingleValueOrNull(IBase theTarget, String thePath) { Class<Object> wantedType = Object.class; return getSingleValueOrNull(theTarget, thePath, wantedType); } public <T> T getSingleValueOrNull(IBase theTarget, String thePath, Class<T> 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("Target is not a composite type: " + theTarget.getClass().getName()); } BaseRuntimeElementCompositeDefinition<?> currentDef = (BaseRuntimeElementCompositeDefinition<?>) def; Object currentObj = theTarget; List<String> parts = Arrays.asList(thePath.split("\\.")); List<T> retVal = getValues(currentDef, currentObj, parts, theWantedType); if (retVal.isEmpty()) { return null; } return retVal.get(0); } @SuppressWarnings("unchecked") private <T> List<T> getValues(BaseRuntimeElementCompositeDefinition<?> theCurrentDef, Object theCurrentObj, List<String> theSubList, Class<T> theWantedClass) { String name = theSubList.get(0); BaseRuntimeChildDefinition nextDef = theCurrentDef.getChildByNameOrThrowDataFormatException(name); List<? extends IBase> values = nextDef.getAccessor().getValues(theCurrentObj); List<T> retVal = new ArrayList<T>(); 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<T> foundValues = getValues(nextChildDef, nextElement, theSubList.subList(1, theSubList.size()), theWantedClass); retVal.addAll(foundValues); } } return retVal; } public List<Object> getValues(IBaseResource theResource, String thePath) { Class<Object> wantedClass = Object.class; return getValues(theResource, thePath, wantedClass); } public <T> List<T> getValues(IBaseResource theResource, String thePath, Class<T> theWantedClass) { RuntimeResourceDefinition def = myContext.getResourceDefinition(theResource); BaseRuntimeElementCompositeDefinition<?> currentDef = def; Object currentObj = theResource; List<String> parts = Arrays.asList(thePath.split("\\.")); List<String> subList = parts.subList(1, parts.size()); if (subList.size() < 1) { throw new ConfigurationException("Invalid path: " + thePath); } return getValues(currentDef, currentObj, subList, theWantedClass); } /** * Returns <code>true</code> if <code>theSource</code> is in the compartment named <code>theCompartmentName</code> * belonging to resource <code>theTarget</code> * * @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 <code>true</code> if <code>theSource</code> 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) { 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; } } List<RuntimeSearchParam> params = sourceDef.getSearchParamsForCompartmentName(theCompartmentName); for (RuntimeSearchParam nextParam : params) { for (String nextPath : nextParam.getPathsSplit()) { for (IBaseReference nextValue : getValues(theSource, nextPath, IBaseReference.class)) { String nextRef = nextValue.getReferenceElement().toUnqualifiedVersionless().getValue(); if (wantRef.equals(nextRef)) { return true; } } } } return false; } private void visit(IBase theElement, BaseRuntimeChildDefinition theChildDefinition, BaseRuntimeElementDefinition<?> theDefinition, IModelVisitor2 theCallback, List<IBase> theContainingElementPath, List<BaseRuntimeChildDefinition> theChildDefinitionPath, List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) { if (theChildDefinition != null) { theChildDefinitionPath.add(theChildDefinition); } theContainingElementPath.add(theElement); theElementDefinitionPath.add(theDefinition); theCallback.acceptElement(theElement, Collections.unmodifiableList(theContainingElementPath), Collections.unmodifiableList(theChildDefinitionPath), Collections.unmodifiableList(theElementDefinitionPath)); /* * 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; childElementDef = nextChild.getChildElementDefinitionByDatatype(nextValue.getClass()); if (childElementDef == null) { StringBuilder b = new StringBuilder(); b.append("Found value of type["); b.append(nextValue.getClass().getSimpleName()); b.append("] which is not valid for field["); b.append(nextChild.getElementName()); b.append("] in "); b.append(childDef.getName()); b.append(" - Valid types: "); for (Iterator<String> iter = new TreeSet<String>(nextChild.getValidChildNames()).iterator(); iter.hasNext();) { BaseRuntimeElementDefinition<?> childByName = nextChild.getChildByName(iter.next()); b.append(childByName.getImplementingClass().getSimpleName()); if (iter.hasNext()) { b.append(", "); } } throw new DataFormatException(b.toString()); } if (nextChild instanceof RuntimeChildDirectResource) { // Don't descend into embedded resources theContainingElementPath.add(nextValue); theChildDefinitionPath.add(nextChild); theElementDefinitionPath.add(myContext.getElementDefinition(nextValue.getClass())); theCallback.acceptElement(nextValue, Collections.unmodifiableList(theContainingElementPath), Collections.unmodifiableList(theChildDefinitionPath), Collections.unmodifiableList(theElementDefinitionPath)); theChildDefinitionPath.remove(theChildDefinitionPath.size() - 1); theContainingElementPath.remove(theContainingElementPath.size() - 1); theElementDefinitionPath.remove(theElementDefinitionPath.size() - 1); } else { 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("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 * * <p> * 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) * </p> * * @param theResource * The resource to visit * @param theVisitor * The visitor */ public void visit(IBaseResource theResource, IModelVisitor theVisitor) { BaseRuntimeElementCompositeDefinition<?> def = myContext.getResourceDefinition(theResource); visit(new IdentityHashMap<Object, Object>(), theResource, theResource, null, null, def, theVisitor); } /** * Visit all elements in a given resource * * THIS ALTERNATE METHOD IS STILL EXPERIMENTAL * * <p> * 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) * </p> * * @param theResource * The resource to visit * @param theVisitor * The visitor */ void visit(IBaseResource theResource, IModelVisitor2 theVisitor) { BaseRuntimeElementCompositeDefinition<?> def = myContext.getResourceDefinition(theResource); visit(theResource, null, def, theVisitor, new ArrayList<IBase>(), new ArrayList<BaseRuntimeChildDefinition>(), new ArrayList<BaseRuntimeElementDefinition<?>>()); } private void visit(IdentityHashMap<Object, Object> theStack, IBaseResource theResource, IBase theElement, List<String> thePathToElement, BaseRuntimeChildDefinition theChildDefinition, BaseRuntimeElementDefinition<?> theDefinition, IModelVisitor theCallback) { List<String> pathToElement = addNameToList(thePathToElement, theChildDefinition); if (theStack.put(theElement, theElement) != null) { return; } theCallback.acceptElement(theResource, theElement, pathToElement, theChildDefinition, theDefinition); BaseRuntimeElementDefinition<?> def = theDefinition; if (def.getChildType() == ChildTypeEnum.CONTAINED_RESOURCE_LIST) { def = myContext.getElementDefinition(theElement.getClass()); } if (theElement instanceof IBaseReference) { IBaseResource target = ((IBaseReference)theElement).getResource(); if (target != null) { if (target.getIdElement().hasIdPart() == false || target.getIdElement().isLocal()) { RuntimeResourceDefinition targetDef = myContext.getResourceDefinition(target); visit(theStack, target, target, pathToElement, null, targetDef, theCallback); } } } switch (def.getChildType()) { case ID_DATATYPE: case PRIMITIVE_XHTML_HL7ORG: case PRIMITIVE_XHTML: case PRIMITIVE_DATATYPE: // These are primitive types break; case RESOURCE: case RESOURCE_BLOCK: case COMPOSITE_DATATYPE: { BaseRuntimeElementCompositeDefinition<?> childDef = (BaseRuntimeElementCompositeDefinition<?>) def; for (BaseRuntimeChildDefinition nextChild : childDef.getChildrenAndExtension()) { List<?> values = nextChild.getAccessor().getValues(theElement); if (values != null) { for (Object nextValueObject : values) { IBase nextValue; try { nextValue = (IBase) nextValueObject; } catch (ClassCastException e) { String s = "Found instance of " + nextValueObject.getClass() + " - Did you set a field value to the incorrect type? Expected " + IBase.class.getName(); throw new ClassCastException(s); } if (nextValue == null) { continue; } if (nextValue.isEmpty()) { continue; } BaseRuntimeElementDefinition<?> childElementDef; childElementDef = nextChild.getChildElementDefinitionByDatatype(nextValue.getClass()); if (childElementDef == null) { childElementDef = myContext.getElementDefinition(nextValue.getClass()); } if (nextChild instanceof RuntimeChildDirectResource) { // Don't descend into embedded resources theCallback.acceptElement(theResource, nextValue, null, nextChild, childElementDef); } else { visit(theStack, theResource, nextValue, pathToElement, nextChild, childElementDef, theCallback); } } } } break; } case CONTAINED_RESOURCES: { BaseContainedDt value = (BaseContainedDt) theElement; for (IResource next : value.getContainedResources()) { def = myContext.getResourceDefinition(next); visit(theStack, next, next, pathToElement, null, def, theCallback); } break; } case CONTAINED_RESOURCE_LIST: case EXTENSION_DECLARED: case UNDECL_EXT: { throw new IllegalStateException("state should not happen: " + def.getChildType()); } } theStack.remove(theElement); } }