Added FHIRPathResource Generator for R4 including Test. (#2310)
Signed-off-by: Marcel Parciak <marcel.parciak@gmail.com>
This commit is contained in:
parent
589eb3158e
commit
d11515599a
|
@ -0,0 +1,609 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 Marcel Parciak <marcel.parciak@med.uni-goettingen.de>
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.hl7.fhir.common.hapi.validation.validator;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Stack;
|
||||||
|
|
||||||
|
import org.hl7.fhir.r4.utils.FHIRPathEngine;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBase;
|
||||||
|
import org.hl7.fhir.instance.model.api.ICompositeType;
|
||||||
|
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||||
|
import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext;
|
||||||
|
import org.hl7.fhir.r4.model.ExpressionNode;
|
||||||
|
import org.hl7.fhir.r4.model.Resource;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
|
||||||
|
import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.context.RuntimeCompositeDatatypeDefinition;
|
||||||
|
import ca.uhn.fhir.context.RuntimePrimitiveDatatypeDefinition;
|
||||||
|
|
||||||
|
public class FHIRPathResourceGeneratorR4<T extends Resource> {
|
||||||
|
|
||||||
|
private FhirContext ctx;
|
||||||
|
private FHIRPathEngine engine;
|
||||||
|
private Map<String, String> pathMapping;
|
||||||
|
private T resource = null;
|
||||||
|
|
||||||
|
private String valueToSet = null;
|
||||||
|
private Stack<GenerationTier> nodeStack = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GenerationTier summarizes some variables that are needed to create FHIR
|
||||||
|
* elements later on.
|
||||||
|
*/
|
||||||
|
class GenerationTier {
|
||||||
|
// The RuntimeDefinition of nodes
|
||||||
|
public BaseRuntimeElementDefinition<?> nodeDefinition = null;
|
||||||
|
// The actual nodes, i.e. the instances that hold the values
|
||||||
|
public List<IBase> nodes = new ArrayList<>();
|
||||||
|
// The ChildDefinition applied to the parent (i.e. one of the nodes from a lower
|
||||||
|
// GenerationTier) to create nodes
|
||||||
|
public BaseRuntimeChildDefinition childDefinition = null;
|
||||||
|
// The path segment name of nodes
|
||||||
|
public String fhirPathName = null;
|
||||||
|
|
||||||
|
public GenerationTier() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public GenerationTier(BaseRuntimeElementDefinition<?> nodeDef, IBase firstNode) {
|
||||||
|
this.nodeDefinition = nodeDef;
|
||||||
|
this.nodes.add(firstNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor without parameters, needs a call to `setMapping` later on in
|
||||||
|
* order to generate any Resources.
|
||||||
|
*/
|
||||||
|
public FHIRPathResourceGeneratorR4() {
|
||||||
|
this.pathMapping = new HashMap<String, String>();
|
||||||
|
this.ctx = FhirContext.forR4();
|
||||||
|
this.engine = new FHIRPathEngine(new HapiWorkerContext(ctx, ctx.getValidationSupport()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor that allows to provide a mapping right away.
|
||||||
|
*
|
||||||
|
* @param mapping Map<String, String> a mapping of FHIRPath to value Strings
|
||||||
|
* that will be used to create a Resource.
|
||||||
|
*/
|
||||||
|
public FHIRPathResourceGeneratorR4(Map<String, String> mapping) {
|
||||||
|
this();
|
||||||
|
this.setMapping(mapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setter for the FHIRPath mapping Map instance.
|
||||||
|
*
|
||||||
|
* @param mapping Map<String, String> a mapping of FHIRPath to value Strings
|
||||||
|
* that will be used to create a Resource.
|
||||||
|
*/
|
||||||
|
public void setMapping(Map<String, String> mapping) {
|
||||||
|
this.pathMapping = mapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Getter for a generated Resource. null if no Resource has been generated yet.
|
||||||
|
*
|
||||||
|
* @return T the generated Resource or null.
|
||||||
|
*/
|
||||||
|
public T getResource() {
|
||||||
|
return this.resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares the internal state prior to generating a FHIR Resource. Called once
|
||||||
|
* upon generation at the start.
|
||||||
|
*
|
||||||
|
* @param resourceClass Class<T> The class of the Resource that shall be created
|
||||||
|
* (an empty Resource will be created in this method).
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private void prepareInternalState(Class<T> resourceClass) {
|
||||||
|
this.resource = (T) this.ctx.getResourceDefinition(resourceClass).newInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The generation method that yields a new instance of class `resourceClass`
|
||||||
|
* with every value set in the FHIRPath mapping.
|
||||||
|
*
|
||||||
|
* @param resourceClass Class<T> The class of the Resource that shall be
|
||||||
|
* created.
|
||||||
|
* @return T a new FHIR Resource instance of class `resourceClass`.
|
||||||
|
*/
|
||||||
|
public T generateResource(Class<T> resourceClass) {
|
||||||
|
this.prepareInternalState(resourceClass);
|
||||||
|
|
||||||
|
for (String fhirPath : this.sortedPaths()) {
|
||||||
|
// prepare the next fhirPath iteration: create a new nodeStack and set the value
|
||||||
|
this.nodeStack = new Stack<>();
|
||||||
|
this.nodeStack.push(new GenerationTier(this.ctx.getResourceDefinition(this.resource), this.resource));
|
||||||
|
this.valueToSet = this.pathMapping.get(fhirPath);
|
||||||
|
|
||||||
|
// pathNode is the part of the FHIRPath we are processing
|
||||||
|
ExpressionNode pathNode = this.engine.parse(fhirPath);
|
||||||
|
while (pathNode != null) {
|
||||||
|
switch (pathNode.getKind()) {
|
||||||
|
case Name:
|
||||||
|
this.handleNameNode(pathNode);
|
||||||
|
break;
|
||||||
|
case Function:
|
||||||
|
this.handleFunctionNode(pathNode);
|
||||||
|
break;
|
||||||
|
case Constant:
|
||||||
|
case Group:
|
||||||
|
case Unary:
|
||||||
|
// TODO: unimplmemented, what to do?
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
pathNode = pathNode.getInner();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.nodeStack = null;
|
||||||
|
return this.resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Handling Named nodes
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a named node, either adding a new layer to the `nodeStack` when
|
||||||
|
* reaching a Composite Node or adding the value for Primitive Nodes.
|
||||||
|
*
|
||||||
|
* @param fhirPath String the FHIRPath section for the next GenerationTier.
|
||||||
|
* @param value String the value that shall be set upon reaching a
|
||||||
|
* PrimitiveNode.
|
||||||
|
*/
|
||||||
|
private void handleNameNode(ExpressionNode fhirPath) {
|
||||||
|
BaseRuntimeChildDefinition childDef = this.nodeStack.peek().nodeDefinition.getChildByName(fhirPath.getName());
|
||||||
|
if (childDef == null) {
|
||||||
|
// nothing to do
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// identify the type of named node we need to handle here by getting the runtime
|
||||||
|
// definition type
|
||||||
|
switch (childDef.getChildByName(fhirPath.getName()).getChildType()) {
|
||||||
|
case COMPOSITE_DATATYPE:
|
||||||
|
handleCompositeNode(fhirPath);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PRIMITIVE_DATATYPE:
|
||||||
|
handlePrimitiveNode(fhirPath);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ID_DATATYPE:
|
||||||
|
case RESOURCE:
|
||||||
|
case CONTAINED_RESOURCE_LIST:
|
||||||
|
case CONTAINED_RESOURCES:
|
||||||
|
case EXTENSION_DECLARED:
|
||||||
|
case PRIMITIVE_XHTML:
|
||||||
|
case PRIMITIVE_XHTML_HL7ORG:
|
||||||
|
case RESOURCE_BLOCK:
|
||||||
|
case UNDECL_EXT:
|
||||||
|
// TODO: not implemented. What to do?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles primitive nodes with regards to the current latest tier of the
|
||||||
|
* nodeStack. Sets a primitive value to all nodes.
|
||||||
|
*
|
||||||
|
* @param fhirPath ExpressionNode segment of the fhirPath that specifies the
|
||||||
|
* primitive value to set.
|
||||||
|
*/
|
||||||
|
private void handlePrimitiveNode(ExpressionNode fhirPath) {
|
||||||
|
// Get the child definition from the parent
|
||||||
|
BaseRuntimeChildDefinition childDefinition = this.nodeStack.peek().nodeDefinition
|
||||||
|
.getChildByName(fhirPath.getName());
|
||||||
|
// Get the primitive type definition from the childDeftinion
|
||||||
|
RuntimePrimitiveDatatypeDefinition primitiveTarget = (RuntimePrimitiveDatatypeDefinition) childDefinition
|
||||||
|
.getChildByName(fhirPath.getName());
|
||||||
|
for (IBase nodeElement : this.nodeStack.peek().nodes) {
|
||||||
|
// add the primitive value to each parent node
|
||||||
|
IPrimitiveType<?> primitive = primitiveTarget
|
||||||
|
.newInstance(childDefinition.getInstanceConstructorArguments());
|
||||||
|
primitive.setValueAsString(this.valueToSet);
|
||||||
|
childDefinition.getMutator().addValue(nodeElement, primitive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a composite node with regards to the current latest tier of the
|
||||||
|
* nodeStack. Creates a new node based on fhirPath if none are available.
|
||||||
|
*
|
||||||
|
* @param fhirPath ExpressionNode the segment of the FHIRPath that is being
|
||||||
|
* handled right now.
|
||||||
|
*/
|
||||||
|
private void handleCompositeNode(ExpressionNode fhirPath) {
|
||||||
|
GenerationTier nextTier = new GenerationTier();
|
||||||
|
// get the name of the FHIRPath for the next tier
|
||||||
|
nextTier.fhirPathName = fhirPath.getName();
|
||||||
|
// get the child definition from the parent nodePefinition
|
||||||
|
nextTier.childDefinition = this.nodeStack.peek().nodeDefinition.getChildByName(fhirPath.getName());
|
||||||
|
// create a nodeDefinition for the next tier
|
||||||
|
nextTier.nodeDefinition = nextTier.childDefinition.getChildByName(nextTier.fhirPathName);
|
||||||
|
|
||||||
|
RuntimeCompositeDatatypeDefinition compositeTarget = (RuntimeCompositeDatatypeDefinition) nextTier.nodeDefinition;
|
||||||
|
// iterate through all parent nodes
|
||||||
|
for (IBase nodeElement : this.nodeStack.peek().nodes) {
|
||||||
|
List<IBase> containedNodes = nextTier.childDefinition.getAccessor().getValues(nodeElement);
|
||||||
|
if (containedNodes.size() > 0) {
|
||||||
|
// check if sister nodes are already available
|
||||||
|
nextTier.nodes.addAll(containedNodes);
|
||||||
|
} else {
|
||||||
|
// if not nodes are available, create a new node
|
||||||
|
ICompositeType compositeNode = compositeTarget
|
||||||
|
.newInstance(nextTier.childDefinition.getInstanceConstructorArguments());
|
||||||
|
nextTier.childDefinition.getMutator().addValue(nodeElement, compositeNode);
|
||||||
|
nextTier.nodes.add(compositeNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// push the created nextTier to the nodeStack
|
||||||
|
this.nodeStack.push(nextTier);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Handling Function Nodes
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a function node of a FHIRPath.
|
||||||
|
*
|
||||||
|
* @param fhirPath ExpressionNode the segment of the FHIRPath that is being
|
||||||
|
* handled right now.
|
||||||
|
*/
|
||||||
|
private void handleFunctionNode(ExpressionNode fhirPath) {
|
||||||
|
switch(fhirPath.getFunction()) {
|
||||||
|
case Where:
|
||||||
|
this.handleWhereFunctionNode(fhirPath);
|
||||||
|
case Aggregate:
|
||||||
|
case Alias:
|
||||||
|
case AliasAs:
|
||||||
|
case All:
|
||||||
|
case AllFalse:
|
||||||
|
case AllTrue:
|
||||||
|
case AnyFalse:
|
||||||
|
case AnyTrue:
|
||||||
|
case As:
|
||||||
|
case Check:
|
||||||
|
case Children:
|
||||||
|
case Combine:
|
||||||
|
case ConformsTo:
|
||||||
|
case Contains:
|
||||||
|
case ConvertsToBoolean:
|
||||||
|
case ConvertsToDateTime:
|
||||||
|
case ConvertsToDecimal:
|
||||||
|
case ConvertsToInteger:
|
||||||
|
case ConvertsToQuantity:
|
||||||
|
case ConvertsToString:
|
||||||
|
case ConvertsToTime:
|
||||||
|
case Count:
|
||||||
|
case Custom:
|
||||||
|
case Descendants:
|
||||||
|
case Distinct:
|
||||||
|
case Empty:
|
||||||
|
case EndsWith:
|
||||||
|
case Exclude:
|
||||||
|
case Exists:
|
||||||
|
case Extension:
|
||||||
|
case First:
|
||||||
|
case HasValue:
|
||||||
|
case HtmlChecks:
|
||||||
|
case Iif:
|
||||||
|
case IndexOf:
|
||||||
|
case Intersect:
|
||||||
|
case Is:
|
||||||
|
case IsDistinct:
|
||||||
|
case Item:
|
||||||
|
case Last:
|
||||||
|
case Length:
|
||||||
|
case Lower:
|
||||||
|
case Matches:
|
||||||
|
case MemberOf:
|
||||||
|
case Not:
|
||||||
|
case Now:
|
||||||
|
case OfType:
|
||||||
|
case Repeat:
|
||||||
|
case Replace:
|
||||||
|
case ReplaceMatches:
|
||||||
|
case Resolve:
|
||||||
|
case Select:
|
||||||
|
case Single:
|
||||||
|
case Skip:
|
||||||
|
case StartsWith:
|
||||||
|
case SubsetOf:
|
||||||
|
case Substring:
|
||||||
|
case SupersetOf:
|
||||||
|
case Tail:
|
||||||
|
case Take:
|
||||||
|
case ToBoolean:
|
||||||
|
case ToChars:
|
||||||
|
case ToDateTime:
|
||||||
|
case ToDecimal:
|
||||||
|
case ToInteger:
|
||||||
|
case ToQuantity:
|
||||||
|
case ToString:
|
||||||
|
case ToTime:
|
||||||
|
case Today:
|
||||||
|
case Trace:
|
||||||
|
case Type:
|
||||||
|
case Union:
|
||||||
|
case Upper:
|
||||||
|
// TODO: unimplemented, what to do?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a function node of a `where`-function. Iterates through all params
|
||||||
|
* and handle where functions for primitive datatypes (others are not
|
||||||
|
* implemented and yield errors.)
|
||||||
|
*
|
||||||
|
* @param fhirPath ExpressionNode the segment of the FHIRPath that contains the
|
||||||
|
* where function
|
||||||
|
*/
|
||||||
|
private void handleWhereFunctionNode(ExpressionNode fhirPath) {
|
||||||
|
// iterate through all where parameters
|
||||||
|
for (ExpressionNode param : fhirPath.getParameters()) {
|
||||||
|
BaseRuntimeChildDefinition wherePropertyChild = this.nodeStack.peek().nodeDefinition
|
||||||
|
.getChildByName(param.getName());
|
||||||
|
BaseRuntimeElementDefinition<?> wherePropertyDefinition = wherePropertyChild
|
||||||
|
.getChildByName(param.getName());
|
||||||
|
|
||||||
|
// only primitive nodes can be checked using the where function
|
||||||
|
switch(wherePropertyDefinition.getChildType()) {
|
||||||
|
case PRIMITIVE_DATATYPE:
|
||||||
|
this.handleWhereFunctionParam(param);
|
||||||
|
break;
|
||||||
|
case COMPOSITE_DATATYPE:
|
||||||
|
case CONTAINED_RESOURCES:
|
||||||
|
case CONTAINED_RESOURCE_LIST:
|
||||||
|
case EXTENSION_DECLARED:
|
||||||
|
case ID_DATATYPE:
|
||||||
|
case PRIMITIVE_XHTML:
|
||||||
|
case PRIMITIVE_XHTML_HL7ORG:
|
||||||
|
case RESOURCE:
|
||||||
|
case RESOURCE_BLOCK:
|
||||||
|
case UNDECL_EXT:
|
||||||
|
// TODO: unimplemented. What to do?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the latest nodeStack tier using `param`.
|
||||||
|
*
|
||||||
|
* @param param ExpressionNode parameter type ExpressionNode that provides the
|
||||||
|
* where clause that is used to filter nodes from the nodeStack.
|
||||||
|
*/
|
||||||
|
private void handleWhereFunctionParam(ExpressionNode param) {
|
||||||
|
BaseRuntimeChildDefinition wherePropertyChild = this.nodeStack.peek().nodeDefinition
|
||||||
|
.getChildByName(param.getName());
|
||||||
|
BaseRuntimeElementDefinition<?> wherePropertyDefinition = wherePropertyChild.getChildByName(param.getName());
|
||||||
|
|
||||||
|
String matchingValue = param.getOpNext().getConstant().toString();
|
||||||
|
List<IBase> matchingNodes = new ArrayList<>();
|
||||||
|
List<IBase> unlabeledNodes = new ArrayList<>();
|
||||||
|
// sort all nodes from the nodeStack into matching nodes and unlabeled nodes
|
||||||
|
for (IBase node : this.nodeStack.peek().nodes) {
|
||||||
|
List<IBase> operationValues = wherePropertyChild.getAccessor().getValues(node);
|
||||||
|
if (operationValues.size() == 0) {
|
||||||
|
unlabeledNodes.add(node);
|
||||||
|
} else {
|
||||||
|
for (IBase operationValue : operationValues) {
|
||||||
|
IPrimitiveType<?> primitive = (IPrimitiveType<?>) operationValue;
|
||||||
|
switch (param.getOperation()) {
|
||||||
|
case Equals:
|
||||||
|
if (primitive.getValueAsString().equals(matchingValue)) {
|
||||||
|
matchingNodes.add(node);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case NotEquals:
|
||||||
|
if (!primitive.getValueAsString().equals(matchingValue)) {
|
||||||
|
matchingNodes.add(node);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case And:
|
||||||
|
case As:
|
||||||
|
case Concatenate:
|
||||||
|
case Contains:
|
||||||
|
case Div:
|
||||||
|
case DivideBy:
|
||||||
|
case Equivalent:
|
||||||
|
case Greater:
|
||||||
|
case GreaterOrEqual:
|
||||||
|
case Implies:
|
||||||
|
case In:
|
||||||
|
case Is:
|
||||||
|
case LessOrEqual:
|
||||||
|
case LessThan:
|
||||||
|
case MemberOf:
|
||||||
|
case Minus:
|
||||||
|
case Mod:
|
||||||
|
case NotEquivalent:
|
||||||
|
case Or:
|
||||||
|
case Plus:
|
||||||
|
case Times:
|
||||||
|
case Union:
|
||||||
|
case Xor:
|
||||||
|
// TODO: unimplemented, what to do?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchingNodes.size() == 0) {
|
||||||
|
if (unlabeledNodes.size() == 0) {
|
||||||
|
// no nodes were matched and no unlabeled nodes are available. We need to add a
|
||||||
|
// sister node to the nodeStack
|
||||||
|
GenerationTier latestTier = this.nodeStack.pop();
|
||||||
|
GenerationTier previousTier = this.nodeStack.peek();
|
||||||
|
this.nodeStack.push(latestTier);
|
||||||
|
|
||||||
|
RuntimeCompositeDatatypeDefinition compositeTarget = (RuntimeCompositeDatatypeDefinition) latestTier.nodeDefinition;
|
||||||
|
ICompositeType compositeNode = compositeTarget
|
||||||
|
.newInstance(latestTier.childDefinition.getInstanceConstructorArguments());
|
||||||
|
latestTier.childDefinition.getMutator().addValue(previousTier.nodes.get(0), compositeNode);
|
||||||
|
unlabeledNodes.add(compositeNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(param.getOperation()) {
|
||||||
|
case Equals:
|
||||||
|
// if we are checking for equality, we need to set the property we looked for on
|
||||||
|
// the unlabeled node(s)
|
||||||
|
RuntimePrimitiveDatatypeDefinition equalsPrimitive = (RuntimePrimitiveDatatypeDefinition) wherePropertyDefinition;
|
||||||
|
IPrimitiveType<?> primitive = equalsPrimitive
|
||||||
|
.newInstance(wherePropertyChild.getInstanceConstructorArguments());
|
||||||
|
primitive.setValueAsString(param.getOpNext().getConstant().toString());
|
||||||
|
for (IBase node : unlabeledNodes) {
|
||||||
|
wherePropertyChild.getMutator().addValue(node, primitive);
|
||||||
|
matchingNodes.add(node);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case NotEquals:
|
||||||
|
// if we are checking for inequality, we need to pass all unlabeled (or created
|
||||||
|
// if none were available)
|
||||||
|
matchingNodes.addAll(unlabeledNodes);
|
||||||
|
break;
|
||||||
|
case And:
|
||||||
|
case As:
|
||||||
|
case Concatenate:
|
||||||
|
case Contains:
|
||||||
|
case Div:
|
||||||
|
case DivideBy:
|
||||||
|
case Equivalent:
|
||||||
|
case Greater:
|
||||||
|
case GreaterOrEqual:
|
||||||
|
case Implies:
|
||||||
|
case In:
|
||||||
|
case Is:
|
||||||
|
case LessOrEqual:
|
||||||
|
case LessThan:
|
||||||
|
case MemberOf:
|
||||||
|
case Minus:
|
||||||
|
case Mod:
|
||||||
|
case NotEquivalent:
|
||||||
|
case Or:
|
||||||
|
case Plus:
|
||||||
|
case Times:
|
||||||
|
case Union:
|
||||||
|
case Xor:
|
||||||
|
// TODO: need to implement above first
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the nodes to the filtered ones
|
||||||
|
this.nodeStack.peek().nodes = matchingNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a list all FHIRPaths from the mapping ordered by paths with where
|
||||||
|
* equals, where unequals and the rest.
|
||||||
|
*
|
||||||
|
* @return List<String> a List of FHIRPaths ordered by the type.
|
||||||
|
*/
|
||||||
|
private List<String> sortedPaths() {
|
||||||
|
List<String> whereEquals = new ArrayList<String>();
|
||||||
|
List<String> whereUnequals = new ArrayList<String>();
|
||||||
|
List<String> withoutWhere = new ArrayList<String>();
|
||||||
|
|
||||||
|
for (String fhirPath : this.pathMapping.keySet()) {
|
||||||
|
switch (this.getTypeOfFhirPath(fhirPath)) {
|
||||||
|
case WHERE_EQUALS:
|
||||||
|
whereEquals.add(fhirPath);
|
||||||
|
break;
|
||||||
|
case WHERE_UNEQUALS:
|
||||||
|
whereUnequals.add(fhirPath);
|
||||||
|
break;
|
||||||
|
case WITHOUT_WHERE:
|
||||||
|
withoutWhere.add(fhirPath);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> ret = new ArrayList<String>();
|
||||||
|
ret.addAll(whereEquals);
|
||||||
|
ret.addAll(whereUnequals);
|
||||||
|
ret.addAll(withoutWhere);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the type of path based on the FHIRPath String.
|
||||||
|
*
|
||||||
|
* @param fhirPath String representation of a FHIRPath.
|
||||||
|
* @return PathType the type of path supplied as `fhirPath`.
|
||||||
|
*/
|
||||||
|
private PathType getTypeOfFhirPath(String fhirPath) {
|
||||||
|
ExpressionNode fhirPathExpression = this.engine.parse(fhirPath);
|
||||||
|
while (fhirPathExpression != null) {
|
||||||
|
if (fhirPathExpression.getKind() == ExpressionNode.Kind.Function) {
|
||||||
|
if (fhirPathExpression.getFunction() == ExpressionNode.Function.Where) {
|
||||||
|
for (ExpressionNode params : fhirPathExpression.getParameters()) {
|
||||||
|
switch (params.getOperation()) {
|
||||||
|
case Equals:
|
||||||
|
return PathType.WHERE_EQUALS;
|
||||||
|
case NotEquals:
|
||||||
|
return PathType.WHERE_UNEQUALS;
|
||||||
|
case And:
|
||||||
|
case As:
|
||||||
|
case Concatenate:
|
||||||
|
case Contains:
|
||||||
|
case Div:
|
||||||
|
case DivideBy:
|
||||||
|
case Equivalent:
|
||||||
|
case Greater:
|
||||||
|
case GreaterOrEqual:
|
||||||
|
case Implies:
|
||||||
|
case In:
|
||||||
|
case Is:
|
||||||
|
case LessOrEqual:
|
||||||
|
case LessThan:
|
||||||
|
case MemberOf:
|
||||||
|
case Minus:
|
||||||
|
case Mod:
|
||||||
|
case NotEquivalent:
|
||||||
|
case Or:
|
||||||
|
case Plus:
|
||||||
|
case Times:
|
||||||
|
case Union:
|
||||||
|
case Xor:
|
||||||
|
// TODO: need to implement above first
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fhirPathExpression = fhirPathExpression.getInner();
|
||||||
|
}
|
||||||
|
return PathType.WITHOUT_WHERE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple enum to diffirentiate between types of FHIRPaths in the special use
|
||||||
|
* case of generating FHIR Resources.
|
||||||
|
*/
|
||||||
|
public enum PathType {
|
||||||
|
WHERE_EQUALS, WHERE_UNEQUALS, WITHOUT_WHERE
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,151 @@
|
||||||
|
package org.hl7.fhir.r4.validation;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.hl7.fhir.common.hapi.validation.validator.FHIRPathResourceGeneratorR4;
|
||||||
|
import org.hl7.fhir.r4.model.Address;
|
||||||
|
import org.hl7.fhir.r4.model.HumanName;
|
||||||
|
import org.hl7.fhir.r4.model.Patient;
|
||||||
|
import org.hl7.fhir.r4.model.Address.AddressType;
|
||||||
|
import org.hl7.fhir.r4.model.Address.AddressUse;
|
||||||
|
import org.hl7.fhir.r4.model.Enumerations.AdministrativeGender;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
public class FHIRPathResourceGeneratorR4Test {
|
||||||
|
|
||||||
|
public Map<String, String> createFhirMapping() {
|
||||||
|
Map<String, String> mapping = new HashMap<>();
|
||||||
|
mapping.put("Patient.name.given", "Marcel");
|
||||||
|
mapping.put("Patient.name.family", "Parciak");
|
||||||
|
mapping.put("Patient.gender", "male");
|
||||||
|
mapping.put("Patient.address.where(use = 'work').city", "Göttingen");
|
||||||
|
mapping.put("Patient.address.where(use = 'work').postalCode", "37075");
|
||||||
|
mapping.put("Patient.address.where(use = 'billing').city", "Göttingen");
|
||||||
|
mapping.put("Patient.address.where(use = 'billing').postalCode", "37099");
|
||||||
|
|
||||||
|
return mapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> createFhirMapping_2() {
|
||||||
|
Map<String, String> mapping = new HashMap<>();
|
||||||
|
mapping.put("Patient.name.where(use = 'official').family", "Parciak");
|
||||||
|
mapping.put("Patient.name.where(use = 'maiden').family", "ParciakMaiden");
|
||||||
|
mapping.put("Patient.name.given", "Marcel");
|
||||||
|
mapping.put("Patient.gender", "male");
|
||||||
|
mapping.put("Patient.address.where(use = 'work').city", "Göttingen");
|
||||||
|
mapping.put("Patient.address.where(use = 'work').postalCode", "37075");
|
||||||
|
|
||||||
|
return mapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void createFhirGenerator() {
|
||||||
|
FHIRPathResourceGeneratorR4<Patient> test = new FHIRPathResourceGeneratorR4<>();
|
||||||
|
assertNotNull(test);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void createFhirGeneratorWithMapping() {
|
||||||
|
Map<String, String> mapping = this.createFhirMapping();
|
||||||
|
FHIRPathResourceGeneratorR4<Patient> test = new FHIRPathResourceGeneratorR4<>(mapping);
|
||||||
|
assertNotNull(test);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void generatePatientResource() {
|
||||||
|
Map<String, String> mapping = this.createFhirMapping();
|
||||||
|
FHIRPathResourceGeneratorR4<Patient> resourceGenerator = new FHIRPathResourceGeneratorR4<>(mapping);
|
||||||
|
Patient patient = resourceGenerator.generateResource(Patient.class);
|
||||||
|
assertNotNull(patient);
|
||||||
|
|
||||||
|
assertEquals(patient.getName().size(), 1);
|
||||||
|
assertEquals(patient.getNameFirstRep().getFamily(), "Parciak");
|
||||||
|
|
||||||
|
assertEquals(patient.getNameFirstRep().getGiven().size(), 1);
|
||||||
|
assertEquals(patient.getNameFirstRep().getGiven().get(0).asStringValue(), "Marcel");
|
||||||
|
|
||||||
|
// note that we have parsed a String here
|
||||||
|
assertEquals(patient.getGender(), AdministrativeGender.MALE);
|
||||||
|
|
||||||
|
assertEquals(patient.getAddress().size(), 2);
|
||||||
|
for(Address address: patient.getAddress()) {
|
||||||
|
assertEquals(address.getCity(), "Göttingen");
|
||||||
|
if(address.getUse() == AddressUse.WORK) {
|
||||||
|
assertEquals(address.getPostalCode(), "37075");
|
||||||
|
} else if(address.getUse() == AddressUse.BILLING) {
|
||||||
|
assertEquals(address.getPostalCode(), "37099");
|
||||||
|
} else {
|
||||||
|
// an address that has no use should not be created based on the test data
|
||||||
|
assertTrue(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void generateMainzellisteDefaultPatient() {
|
||||||
|
Map<String, String> mapping = this.createFhirMapping_2();
|
||||||
|
FHIRPathResourceGeneratorR4<Patient> resourceGenerator = new FHIRPathResourceGeneratorR4<>(mapping);
|
||||||
|
Patient patient = resourceGenerator.generateResource(Patient.class);
|
||||||
|
assertNotNull(patient);
|
||||||
|
|
||||||
|
assertEquals(patient.getName().size(), 2);
|
||||||
|
for(HumanName name: patient.getName()) {
|
||||||
|
assertEquals(name.getGiven().size(), 1);
|
||||||
|
assertEquals(name.getGiven().get(0).asStringValue(), "Marcel");
|
||||||
|
if(name.getUse() == HumanName.NameUse.OFFICIAL) {
|
||||||
|
assertEquals(name.getFamily(), "Parciak");
|
||||||
|
} else if (name.getUse() == HumanName.NameUse.MAIDEN) {
|
||||||
|
assertEquals(name.getFamily(), "ParciakMaiden");
|
||||||
|
} else {
|
||||||
|
assertTrue(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// note that we have parsed a String here
|
||||||
|
assertEquals(patient.getGender(), AdministrativeGender.MALE);
|
||||||
|
|
||||||
|
assertEquals(patient.getAddress().size(), 1);
|
||||||
|
assertEquals(patient.getAddressFirstRep().getUse(), AddressUse.WORK);
|
||||||
|
assertEquals(patient.getAddressFirstRep().getPostalCode(), "37075");
|
||||||
|
assertEquals(patient.getAddressFirstRep().getCity(), "Göttingen");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void checkGenerationWithWhereUnequal() {
|
||||||
|
Map<String, String> mapping = this.createFhirMapping();
|
||||||
|
mapping.put("Patient.address.where(use = 'billing').type", "postal");
|
||||||
|
mapping.put("Patient.address.where(use != 'billing').type", "physical");
|
||||||
|
FHIRPathResourceGeneratorR4<Patient> resourceGenerator = new FHIRPathResourceGeneratorR4<>(mapping);
|
||||||
|
Patient patient = resourceGenerator.generateResource(Patient.class);
|
||||||
|
assertNotNull(patient);
|
||||||
|
|
||||||
|
assertEquals(patient.getName().size(), 1);
|
||||||
|
assertEquals(patient.getNameFirstRep().getFamily(), "Parciak");
|
||||||
|
|
||||||
|
assertEquals(patient.getNameFirstRep().getGiven().size(), 1);
|
||||||
|
assertEquals(patient.getNameFirstRep().getGiven().get(0).asStringValue(), "Marcel");
|
||||||
|
|
||||||
|
// note that we have parsed a String here
|
||||||
|
assertEquals(patient.getGender(), AdministrativeGender.MALE);
|
||||||
|
|
||||||
|
assertEquals(patient.getAddress().size(), 2);
|
||||||
|
for(Address address: patient.getAddress()) {
|
||||||
|
assertEquals(address.getCity(), "Göttingen");
|
||||||
|
if(address.getUse() == AddressUse.WORK) {
|
||||||
|
assertEquals(address.getPostalCode(), "37075");
|
||||||
|
assertEquals(address.getType(), AddressType.PHYSICAL);
|
||||||
|
} else if(address.getUse() == AddressUse.BILLING) {
|
||||||
|
assertEquals(address.getPostalCode(), "37099");
|
||||||
|
assertEquals(address.getType(), AddressType.POSTAL);
|
||||||
|
} else {
|
||||||
|
// an address that has no use should not be created based on the test data
|
||||||
|
assertTrue(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue