001package org.hl7.fhir.common.hapi.validation.validator;
002
003import java.util.ArrayList;
004import java.util.HashMap;
005import java.util.List;
006import java.util.Map;
007import java.util.Stack;
008
009import org.hl7.fhir.r4.utils.FHIRPathEngine;
010import org.hl7.fhir.instance.model.api.IBase;
011import org.hl7.fhir.instance.model.api.ICompositeType;
012import org.hl7.fhir.instance.model.api.IPrimitiveType;
013import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext;
014import org.hl7.fhir.r4.model.ExpressionNode;
015import org.hl7.fhir.r4.model.Resource;
016
017import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
018import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
019import ca.uhn.fhir.context.FhirContext;
020import ca.uhn.fhir.context.RuntimeCompositeDatatypeDefinition;
021import ca.uhn.fhir.context.RuntimePrimitiveDatatypeDefinition;
022
023/**
024 * This class can be used to generate resources using FHIRPath expressions.
025 *
026 * Note that this is an experimental feature and the API is expected to change. Ideally
027 * this will be made version independent and moved out of the validation module
028 * in a future release.
029 *
030 * @author Marcel Parciak <marcel.parciak@med.uni-goettingen.de>
031 */
032public class FHIRPathResourceGeneratorR4<T extends Resource> {
033
034    private FhirContext ctx;
035    private FHIRPathEngine engine;
036    private Map<String, String> pathMapping;
037    private T resource = null;
038
039    private String valueToSet = null;
040    private Stack<GenerationTier> nodeStack = null;
041
042    /**
043     * The GenerationTier summarizes some variables that are needed to create FHIR
044     * elements later on.
045     */
046    class GenerationTier {
047        // The RuntimeDefinition of nodes
048        public BaseRuntimeElementDefinition<?> nodeDefinition = null;
049        // The actual nodes, i.e. the instances that hold the values
050        public List<IBase> nodes = new ArrayList<>();
051        // The ChildDefinition applied to the parent (i.e. one of the nodes from a lower
052        // GenerationTier) to create nodes
053        public BaseRuntimeChildDefinition childDefinition = null;
054        // The path segment name of nodes
055        public String fhirPathName = null;
056
057        public GenerationTier() {
058        }
059
060        public GenerationTier(BaseRuntimeElementDefinition<?> nodeDef, IBase firstNode) {
061            this.nodeDefinition = nodeDef;
062            this.nodes.add(firstNode);
063        }
064    }
065
066    /**
067     * Constructor without parameters, needs a call to `setMapping` later on in
068     * order to generate any Resources.
069     */
070    public FHIRPathResourceGeneratorR4() {
071        this.pathMapping = new HashMap<String, String>();
072        this.ctx = FhirContext.forR4();
073        this.engine = new FHIRPathEngine(new HapiWorkerContext(ctx, ctx.getValidationSupport()));
074    }
075
076    /**
077     * Constructor that allows to provide a mapping right away.
078     * 
079     * @param mapping Map<String, String> a mapping of FHIRPath to value Strings
080     *                that will be used to create a Resource.
081     */
082    public FHIRPathResourceGeneratorR4(Map<String, String> mapping) {
083        this();
084        this.setMapping(mapping);
085    }
086
087    /**
088     * Setter for the FHIRPath mapping Map instance.
089     * 
090     * @param mapping Map<String, String> a mapping of FHIRPath to value Strings
091     *                that will be used to create a Resource.
092     */
093    public void setMapping(Map<String, String> mapping) {
094        this.pathMapping = mapping;
095    }
096
097    /**
098     * Getter for a generated Resource. null if no Resource has been generated yet.
099     * 
100     * @return T the generated Resource or null.
101     */
102    public T getResource() {
103        return this.resource;
104    }
105
106    /**
107     * Prepares the internal state prior to generating a FHIR Resource. Called once
108     * upon generation at the start.
109     * 
110     * @param resourceClass Class<T> The class of the Resource that shall be created
111     *                      (an empty Resource will be created in this method).
112     */
113    @SuppressWarnings("unchecked")
114    private void prepareInternalState(Class<T> resourceClass) {
115        this.resource = (T) this.ctx.getResourceDefinition(resourceClass).newInstance();
116    }
117
118    /**
119     * The generation method that yields a new instance of class `resourceClass`
120     * with every value set in the FHIRPath mapping.
121     * 
122     * @param resourceClass Class<T> The class of the Resource that shall be
123     *                      created.
124     * @return T a new FHIR Resource instance of class `resourceClass`.
125     */
126    public T generateResource(Class<T> resourceClass) {
127        this.prepareInternalState(resourceClass);
128
129        for (String fhirPath : this.sortedPaths()) {
130            // prepare the next fhirPath iteration: create a new nodeStack and set the value
131            this.nodeStack = new Stack<>();
132            this.nodeStack.push(new GenerationTier(this.ctx.getResourceDefinition(this.resource), this.resource));
133            this.valueToSet = this.pathMapping.get(fhirPath);
134
135            // pathNode is the part of the FHIRPath we are processing
136            ExpressionNode pathNode = this.engine.parse(fhirPath);
137            while (pathNode != null) {
138                switch (pathNode.getKind()) {
139                    case Name:
140                        this.handleNameNode(pathNode);
141                        break;
142                    case Function:
143                        this.handleFunctionNode(pathNode);
144                        break;
145                    case Constant:
146                    case Group:
147                    case Unary:
148                        // TODO: unimplmemented, what to do?
149                        break;
150                }
151                pathNode = pathNode.getInner();
152            }
153        }
154
155        this.nodeStack = null;
156        return this.resource;
157    }
158
159    /*
160     * Handling Named nodes
161     */
162
163    /**
164     * Handles a named node, either adding a new layer to the `nodeStack` when
165     * reaching a Composite Node or adding the value for Primitive Nodes.
166     * 
167     * @param fhirPath String the FHIRPath section for the next GenerationTier.
168     * @param value    String the value that shall be set upon reaching a
169     *                 PrimitiveNode.
170     */
171    private void handleNameNode(ExpressionNode fhirPath) {
172        BaseRuntimeChildDefinition childDef = this.nodeStack.peek().nodeDefinition.getChildByName(fhirPath.getName());
173        if (childDef == null) {
174            // nothing to do
175            return;
176        }
177
178        // identify the type of named node we need to handle here by getting the runtime
179        // definition type
180        switch (childDef.getChildByName(fhirPath.getName()).getChildType()) {
181            case COMPOSITE_DATATYPE:
182                handleCompositeNode(fhirPath);
183                break;
184
185            case PRIMITIVE_DATATYPE:
186                handlePrimitiveNode(fhirPath);
187                break;
188
189            case ID_DATATYPE:
190            case RESOURCE:
191            case CONTAINED_RESOURCE_LIST:
192            case CONTAINED_RESOURCES:
193            case EXTENSION_DECLARED:
194            case PRIMITIVE_XHTML:
195            case PRIMITIVE_XHTML_HL7ORG:
196            case RESOURCE_BLOCK:
197            case UNDECL_EXT:
198                // TODO: not implemented. What to do?
199        }
200    }
201
202    /**
203     * Handles primitive nodes with regards to the current latest tier of the
204     * nodeStack. Sets a primitive value to all nodes.
205     * 
206     * @param fhirPath ExpressionNode segment of the fhirPath that specifies the
207     *                 primitive value to set.
208     */
209    private void handlePrimitiveNode(ExpressionNode fhirPath) {
210        // Get the child definition from the parent
211        BaseRuntimeChildDefinition childDefinition = this.nodeStack.peek().nodeDefinition
212                .getChildByName(fhirPath.getName());
213        // Get the primitive type definition from the childDeftinion
214        RuntimePrimitiveDatatypeDefinition primitiveTarget = (RuntimePrimitiveDatatypeDefinition) childDefinition
215                .getChildByName(fhirPath.getName());
216        for (IBase nodeElement : this.nodeStack.peek().nodes) {
217            // add the primitive value to each parent node
218            IPrimitiveType<?> primitive = primitiveTarget
219                    .newInstance(childDefinition.getInstanceConstructorArguments());
220            primitive.setValueAsString(this.valueToSet);
221            childDefinition.getMutator().addValue(nodeElement, primitive);
222        }
223    }
224
225    /**
226     * Handles a composite node with regards to the current latest tier of the
227     * nodeStack. Creates a new node based on fhirPath if none are available.
228     * 
229     * @param fhirPath ExpressionNode the segment of the FHIRPath that is being
230     *                 handled right now.
231     */
232    private void handleCompositeNode(ExpressionNode fhirPath) {
233        GenerationTier nextTier = new GenerationTier();
234        // get the name of the FHIRPath for the next tier
235        nextTier.fhirPathName = fhirPath.getName();
236        // get the child definition from the parent nodePefinition
237        nextTier.childDefinition = this.nodeStack.peek().nodeDefinition.getChildByName(fhirPath.getName());
238        // create a nodeDefinition for the next tier
239        nextTier.nodeDefinition = nextTier.childDefinition.getChildByName(nextTier.fhirPathName);
240
241        RuntimeCompositeDatatypeDefinition compositeTarget = (RuntimeCompositeDatatypeDefinition) nextTier.nodeDefinition;
242        // iterate through all parent nodes
243        for (IBase nodeElement : this.nodeStack.peek().nodes) {
244            List<IBase> containedNodes = nextTier.childDefinition.getAccessor().getValues(nodeElement);
245            if (containedNodes.size() > 0) {
246                // check if sister nodes are already available
247                nextTier.nodes.addAll(containedNodes);
248            } else {
249                // if not nodes are available, create a new node
250                ICompositeType compositeNode = compositeTarget
251                        .newInstance(nextTier.childDefinition.getInstanceConstructorArguments());
252                nextTier.childDefinition.getMutator().addValue(nodeElement, compositeNode);
253                nextTier.nodes.add(compositeNode);
254            }
255        }
256        // push the created nextTier to the nodeStack
257        this.nodeStack.push(nextTier);
258    }
259
260    /*
261     * Handling Function Nodes
262     */
263
264    /**
265     * Handles a function node of a FHIRPath.
266     * 
267     * @param fhirPath ExpressionNode the segment of the FHIRPath that is being
268     *                 handled right now.
269     */
270    private void handleFunctionNode(ExpressionNode fhirPath) {
271        switch(fhirPath.getFunction()) {
272            case Where:
273                this.handleWhereFunctionNode(fhirPath);
274            case Aggregate:
275            case Alias:
276            case AliasAs:
277            case All:
278            case AllFalse:
279            case AllTrue:
280            case AnyFalse:
281            case AnyTrue:
282            case As:
283            case Check:
284            case Children:
285            case Combine:
286            case ConformsTo:
287            case Contains:
288            case ConvertsToBoolean:
289            case ConvertsToDateTime:
290            case ConvertsToDecimal:
291            case ConvertsToInteger:
292            case ConvertsToQuantity:
293            case ConvertsToString:
294            case ConvertsToTime:
295            case Count:
296            case Custom:
297            case Descendants:
298            case Distinct:
299            case Empty:
300            case EndsWith:
301            case Exclude:
302            case Exists:
303            case Extension:
304            case First:
305            case HasValue:
306            case HtmlChecks:
307            case Iif:
308            case IndexOf:
309            case Intersect:
310            case Is:
311            case IsDistinct:
312            case Item:
313            case Last:
314            case Length:
315            case Lower:
316            case Matches:
317            case MemberOf:
318            case Not:
319            case Now:
320            case OfType:
321            case Repeat:
322            case Replace:
323            case ReplaceMatches:
324            case Resolve:
325            case Select:
326            case Single:
327            case Skip:
328            case StartsWith:
329            case SubsetOf:
330            case Substring:
331            case SupersetOf:
332            case Tail:
333            case Take:
334            case ToBoolean:
335            case ToChars:
336            case ToDateTime:
337            case ToDecimal:
338            case ToInteger:
339            case ToQuantity:
340            case ToString:
341            case ToTime:
342            case Today:
343            case Trace:
344            case Type:
345            case Union:
346            case Upper:
347                // TODO: unimplemented, what to do?
348        }
349    }
350
351    /**
352     * Handles a function node of a `where`-function. Iterates through all params
353     * and handle where functions for primitive datatypes (others are not
354     * implemented and yield errors.)
355     * 
356     * @param fhirPath ExpressionNode the segment of the FHIRPath that contains the
357     *                 where function
358     */
359    private void handleWhereFunctionNode(ExpressionNode fhirPath) {
360        // iterate through all where parameters
361        for (ExpressionNode param : fhirPath.getParameters()) {
362            BaseRuntimeChildDefinition wherePropertyChild = this.nodeStack.peek().nodeDefinition
363                    .getChildByName(param.getName());
364            BaseRuntimeElementDefinition<?> wherePropertyDefinition = wherePropertyChild
365                    .getChildByName(param.getName());
366
367            // only primitive nodes can be checked using the where function
368            switch(wherePropertyDefinition.getChildType()) {
369                case PRIMITIVE_DATATYPE:
370                    this.handleWhereFunctionParam(param);
371                    break;
372                case COMPOSITE_DATATYPE:
373                case CONTAINED_RESOURCES:
374                case CONTAINED_RESOURCE_LIST:
375                case EXTENSION_DECLARED:
376                case ID_DATATYPE:
377                case PRIMITIVE_XHTML:
378                case PRIMITIVE_XHTML_HL7ORG:
379                case RESOURCE:
380                case RESOURCE_BLOCK:
381                case UNDECL_EXT:
382                    // TODO: unimplemented. What to do?
383            }
384        }
385    }
386
387    /**
388     * Filter the latest nodeStack tier using `param`.
389     * 
390     * @param param ExpressionNode parameter type ExpressionNode that provides the
391     *              where clause that is used to filter nodes from the nodeStack.
392     */
393    private void handleWhereFunctionParam(ExpressionNode param) {
394        BaseRuntimeChildDefinition wherePropertyChild = this.nodeStack.peek().nodeDefinition
395                .getChildByName(param.getName());
396        BaseRuntimeElementDefinition<?> wherePropertyDefinition = wherePropertyChild.getChildByName(param.getName());
397
398        String matchingValue = param.getOpNext().getConstant().toString();
399        List<IBase> matchingNodes = new ArrayList<>();
400        List<IBase> unlabeledNodes = new ArrayList<>();
401        // sort all nodes from the nodeStack into matching nodes and unlabeled nodes
402        for (IBase node : this.nodeStack.peek().nodes) {
403            List<IBase> operationValues = wherePropertyChild.getAccessor().getValues(node);
404            if (operationValues.size() == 0) {
405                unlabeledNodes.add(node);
406            } else {
407                for (IBase operationValue : operationValues) {
408                    IPrimitiveType<?> primitive = (IPrimitiveType<?>) operationValue;
409                    switch (param.getOperation()) {
410                        case Equals:
411                            if (primitive.getValueAsString().equals(matchingValue)) {
412                                matchingNodes.add(node);
413                            }
414                            break;
415                        case NotEquals:
416                            if (!primitive.getValueAsString().equals(matchingValue)) {
417                                matchingNodes.add(node);
418                            }
419                            break;
420                        case And:
421                        case As:
422                        case Concatenate:
423                        case Contains:
424                        case Div:
425                        case DivideBy:
426                        case Equivalent:
427                        case Greater:
428                        case GreaterOrEqual:
429                        case Implies:
430                        case In:
431                        case Is:
432                        case LessOrEqual:
433                        case LessThan:
434                        case MemberOf:
435                        case Minus:
436                        case Mod:
437                        case NotEquivalent:
438                        case Or:
439                        case Plus:
440                        case Times:
441                        case Union:
442                        case Xor:
443                            // TODO: unimplemented, what to do?
444                    }
445                }
446            }
447        }
448
449        if (matchingNodes.size() == 0) {
450            if (unlabeledNodes.size() == 0) {
451                // no nodes were matched and no unlabeled nodes are available. We need to add a
452                // sister node to the nodeStack
453                GenerationTier latestTier = this.nodeStack.pop();
454                GenerationTier previousTier = this.nodeStack.peek();
455                this.nodeStack.push(latestTier);
456
457                RuntimeCompositeDatatypeDefinition compositeTarget = (RuntimeCompositeDatatypeDefinition) latestTier.nodeDefinition;
458                ICompositeType compositeNode = compositeTarget
459                        .newInstance(latestTier.childDefinition.getInstanceConstructorArguments());
460                latestTier.childDefinition.getMutator().addValue(previousTier.nodes.get(0), compositeNode);
461                unlabeledNodes.add(compositeNode);
462            }
463
464            switch(param.getOperation()) {
465                case Equals:
466                    // if we are checking for equality, we need to set the property we looked for on
467                    // the unlabeled node(s)
468                    RuntimePrimitiveDatatypeDefinition equalsPrimitive = (RuntimePrimitiveDatatypeDefinition) wherePropertyDefinition;
469                    IPrimitiveType<?> primitive = equalsPrimitive
470                            .newInstance(wherePropertyChild.getInstanceConstructorArguments());
471                    primitive.setValueAsString(param.getOpNext().getConstant().toString());
472                    for (IBase node : unlabeledNodes) {
473                        wherePropertyChild.getMutator().addValue(node, primitive);
474                        matchingNodes.add(node);
475                    }
476                    break;
477                case NotEquals:
478                    // if we are checking for inequality, we need to pass all unlabeled (or created
479                    // if none were available)
480                    matchingNodes.addAll(unlabeledNodes);
481                    break;
482                case And:
483                case As:
484                case Concatenate:
485                case Contains:
486                case Div:
487                case DivideBy:
488                case Equivalent:
489                case Greater:
490                case GreaterOrEqual:
491                case Implies:
492                case In:
493                case Is:
494                case LessOrEqual:
495                case LessThan:
496                case MemberOf:
497                case Minus:
498                case Mod:
499                case NotEquivalent:
500                case Or:
501                case Plus:
502                case Times:
503                case Union:
504                case Xor:
505                    // TODO: need to implement above first
506            }
507        }
508
509        // set the nodes to the filtered ones
510        this.nodeStack.peek().nodes = matchingNodes;
511    }
512
513    /**
514     * Creates a list all FHIRPaths from the mapping ordered by paths with where
515     * equals, where unequals and the rest.
516     * 
517     * @return List<String> a List of FHIRPaths ordered by the type.
518     */
519    private List<String> sortedPaths() {
520        List<String> whereEquals = new ArrayList<String>();
521        List<String> whereUnequals = new ArrayList<String>();
522        List<String> withoutWhere = new ArrayList<String>();
523
524        for (String fhirPath : this.pathMapping.keySet()) {
525            switch (this.getTypeOfFhirPath(fhirPath)) {
526                case WHERE_EQUALS:
527                    whereEquals.add(fhirPath);
528                    break;
529                case WHERE_UNEQUALS:
530                    whereUnequals.add(fhirPath);
531                    break;
532                case WITHOUT_WHERE:
533                    withoutWhere.add(fhirPath);
534                    break;
535            }
536        }
537
538        List<String> ret = new ArrayList<String>();
539        ret.addAll(whereEquals);
540        ret.addAll(whereUnequals);
541        ret.addAll(withoutWhere);
542        return ret;
543    }
544
545    /**
546     * Returns the type of path based on the FHIRPath String.
547     * 
548     * @param fhirPath String representation of a FHIRPath.
549     * @return PathType the type of path supplied as `fhirPath`.
550     */
551    private PathType getTypeOfFhirPath(String fhirPath) {
552        ExpressionNode fhirPathExpression = this.engine.parse(fhirPath);
553        while (fhirPathExpression != null) {
554            if (fhirPathExpression.getKind() == ExpressionNode.Kind.Function) {
555                if (fhirPathExpression.getFunction() == ExpressionNode.Function.Where) {
556                    for (ExpressionNode params : fhirPathExpression.getParameters()) {
557                        switch (params.getOperation()) {
558                            case Equals:
559                                return PathType.WHERE_EQUALS;
560                            case NotEquals:
561                                return PathType.WHERE_UNEQUALS;
562                            case And:
563                            case As:
564                            case Concatenate:
565                            case Contains:
566                            case Div:
567                            case DivideBy:
568                            case Equivalent:
569                            case Greater:
570                            case GreaterOrEqual:
571                            case Implies:
572                            case In:
573                            case Is:
574                            case LessOrEqual:
575                            case LessThan:
576                            case MemberOf:
577                            case Minus:
578                            case Mod:
579                            case NotEquivalent:
580                            case Or:
581                            case Plus:
582                            case Times:
583                            case Union:
584                            case Xor:
585                                // TODO: need to implement above first
586                        }
587                    }
588                }
589            }
590            fhirPathExpression = fhirPathExpression.getInner();
591        }
592        return PathType.WITHOUT_WHERE;
593    }
594
595    /**
596     * A simple enum to diffirentiate between types of FHIRPaths in the special use
597     * case of generating FHIR Resources.
598     */
599    public enum PathType {
600        WHERE_EQUALS, WHERE_UNEQUALS, WITHOUT_WHERE
601    }
602}