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     */
169    private void handleNameNode(ExpressionNode fhirPath) {
170        BaseRuntimeChildDefinition childDef = this.nodeStack.peek().nodeDefinition.getChildByName(fhirPath.getName());
171        if (childDef == null) {
172            // nothing to do
173            return;
174        }
175
176        // identify the type of named node we need to handle here by getting the runtime
177        // definition type
178        switch (childDef.getChildByName(fhirPath.getName()).getChildType()) {
179            case COMPOSITE_DATATYPE:
180                handleCompositeNode(fhirPath);
181                break;
182
183            case PRIMITIVE_DATATYPE:
184                handlePrimitiveNode(fhirPath);
185                break;
186
187            case ID_DATATYPE:
188            case RESOURCE:
189            case CONTAINED_RESOURCE_LIST:
190            case CONTAINED_RESOURCES:
191            case EXTENSION_DECLARED:
192            case PRIMITIVE_XHTML:
193            case PRIMITIVE_XHTML_HL7ORG:
194            case RESOURCE_BLOCK:
195            case UNDECL_EXT:
196                // TODO: not implemented. What to do?
197        }
198    }
199
200    /**
201     * Handles primitive nodes with regards to the current latest tier of the
202     * nodeStack. Sets a primitive value to all nodes.
203     * 
204     * @param fhirPath ExpressionNode segment of the fhirPath that specifies the
205     *                 primitive value to set.
206     */
207    private void handlePrimitiveNode(ExpressionNode fhirPath) {
208        // Get the child definition from the parent
209        BaseRuntimeChildDefinition childDefinition = this.nodeStack.peek().nodeDefinition
210                .getChildByName(fhirPath.getName());
211        // Get the primitive type definition from the childDeftinion
212        RuntimePrimitiveDatatypeDefinition primitiveTarget = (RuntimePrimitiveDatatypeDefinition) childDefinition
213                .getChildByName(fhirPath.getName());
214        for (IBase nodeElement : this.nodeStack.peek().nodes) {
215            // add the primitive value to each parent node
216            IPrimitiveType<?> primitive = primitiveTarget
217                    .newInstance(childDefinition.getInstanceConstructorArguments());
218            primitive.setValueAsString(this.valueToSet);
219            childDefinition.getMutator().addValue(nodeElement, primitive);
220        }
221    }
222
223    /**
224     * Handles a composite node with regards to the current latest tier of the
225     * nodeStack. Creates a new node based on fhirPath if none are available.
226     * 
227     * @param fhirPath ExpressionNode the segment of the FHIRPath that is being
228     *                 handled right now.
229     */
230    private void handleCompositeNode(ExpressionNode fhirPath) {
231        GenerationTier nextTier = new GenerationTier();
232        // get the name of the FHIRPath for the next tier
233        nextTier.fhirPathName = fhirPath.getName();
234        // get the child definition from the parent nodePefinition
235        nextTier.childDefinition = this.nodeStack.peek().nodeDefinition.getChildByName(fhirPath.getName());
236        // create a nodeDefinition for the next tier
237        nextTier.nodeDefinition = nextTier.childDefinition.getChildByName(nextTier.fhirPathName);
238
239        RuntimeCompositeDatatypeDefinition compositeTarget = (RuntimeCompositeDatatypeDefinition) nextTier.nodeDefinition;
240        // iterate through all parent nodes
241        for (IBase nodeElement : this.nodeStack.peek().nodes) {
242            List<IBase> containedNodes = nextTier.childDefinition.getAccessor().getValues(nodeElement);
243            if (containedNodes.size() > 0) {
244                // check if sister nodes are already available
245                nextTier.nodes.addAll(containedNodes);
246            } else {
247                // if not nodes are available, create a new node
248                ICompositeType compositeNode = compositeTarget
249                        .newInstance(nextTier.childDefinition.getInstanceConstructorArguments());
250                nextTier.childDefinition.getMutator().addValue(nodeElement, compositeNode);
251                nextTier.nodes.add(compositeNode);
252            }
253        }
254        // push the created nextTier to the nodeStack
255        this.nodeStack.push(nextTier);
256    }
257
258    /*
259     * Handling Function Nodes
260     */
261
262    /**
263     * Handles a function node of a FHIRPath.
264     * 
265     * @param fhirPath ExpressionNode the segment of the FHIRPath that is being
266     *                 handled right now.
267     */
268    private void handleFunctionNode(ExpressionNode fhirPath) {
269        switch(fhirPath.getFunction()) {
270            case Where:
271                this.handleWhereFunctionNode(fhirPath);
272                                         break;
273                          case MatchesFull:
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 Iif:
307            case IndexOf:
308            case Intersect:
309            case Is:
310            case IsDistinct:
311            case Item:
312            case Last:
313            case Length:
314            case Lower:
315            case Matches:
316            case MemberOf:
317            case Not:
318            case Now:
319            case OfType:
320            case Repeat:
321            case Replace:
322            case ReplaceMatches:
323            case Resolve:
324            case Select:
325            case Single:
326            case Skip:
327            case StartsWith:
328            case SubsetOf:
329            case Substring:
330            case SupersetOf:
331            case Tail:
332            case Take:
333            case ToBoolean:
334            case ToChars:
335            case ToDateTime:
336            case ToDecimal:
337            case ToInteger:
338            case ToQuantity:
339            case ToString:
340            case ToTime:
341            case Today:
342            case Trace:
343            case Type:
344            case Union:
345            case Upper:
346                // TODO: unimplemented, what to do?
347                          case ConvertsToDate:
348                                  break;
349                          case Round:
350                                  break;
351                          case Sqrt:
352                                  break;
353                          case Abs:
354                                  break;
355                          case Ceiling:
356                                  break;
357                          case Exp:
358                                  break;
359                          case Floor:
360                                  break;
361                          case Ln:
362                                  break;
363                          case Log:
364                                  break;
365                          case Power:
366                                  break;
367                          case Truncate:
368                                  break;
369                          case Encode:
370                                  break;
371                          case Decode:
372                                  break;
373                          case Escape:
374                                  break;
375                          case Unescape:
376                                  break;
377                          case Trim:
378                                  break;
379                          case Split:
380                                  break;
381                          case Join:
382                                  break;
383                          case LowBoundary:
384                                  break;
385                          case HighBoundary:
386                                  break;
387                          case Precision:
388                                  break;
389                          case HtmlChecks1:
390                                  break;
391                          case HtmlChecks2:
392                                  break;
393                  }
394    }
395
396    /**
397     * Handles a function node of a `where`-function. Iterates through all params
398     * and handle where functions for primitive datatypes (others are not
399     * implemented and yield errors.)
400     * 
401     * @param fhirPath ExpressionNode the segment of the FHIRPath that contains the
402     *                 where function
403     */
404    private void handleWhereFunctionNode(ExpressionNode fhirPath) {
405        // iterate through all where parameters
406        for (ExpressionNode param : fhirPath.getParameters()) {
407            BaseRuntimeChildDefinition wherePropertyChild = this.nodeStack.peek().nodeDefinition
408                    .getChildByName(param.getName());
409            BaseRuntimeElementDefinition<?> wherePropertyDefinition = wherePropertyChild
410                    .getChildByName(param.getName());
411
412            // only primitive nodes can be checked using the where function
413            switch(wherePropertyDefinition.getChildType()) {
414                case PRIMITIVE_DATATYPE:
415                    this.handleWhereFunctionParam(param);
416                    break;
417                case COMPOSITE_DATATYPE:
418                case CONTAINED_RESOURCES:
419                case CONTAINED_RESOURCE_LIST:
420                case EXTENSION_DECLARED:
421                case ID_DATATYPE:
422                case PRIMITIVE_XHTML:
423                case PRIMITIVE_XHTML_HL7ORG:
424                case RESOURCE:
425                case RESOURCE_BLOCK:
426                case UNDECL_EXT:
427                    // TODO: unimplemented. What to do?
428            }
429        }
430    }
431
432    /**
433     * Filter the latest nodeStack tier using `param`.
434     * 
435     * @param param ExpressionNode parameter type ExpressionNode that provides the
436     *              where clause that is used to filter nodes from the nodeStack.
437     */
438    private void handleWhereFunctionParam(ExpressionNode param) {
439        BaseRuntimeChildDefinition wherePropertyChild = this.nodeStack.peek().nodeDefinition
440                .getChildByName(param.getName());
441        BaseRuntimeElementDefinition<?> wherePropertyDefinition = wherePropertyChild.getChildByName(param.getName());
442
443        String matchingValue = param.getOpNext().getConstant().toString();
444        List<IBase> matchingNodes = new ArrayList<>();
445        List<IBase> unlabeledNodes = new ArrayList<>();
446        // sort all nodes from the nodeStack into matching nodes and unlabeled nodes
447        for (IBase node : this.nodeStack.peek().nodes) {
448            List<IBase> operationValues = wherePropertyChild.getAccessor().getValues(node);
449            if (operationValues.size() == 0) {
450                unlabeledNodes.add(node);
451            } else {
452                for (IBase operationValue : operationValues) {
453                    IPrimitiveType<?> primitive = (IPrimitiveType<?>) operationValue;
454                    switch (param.getOperation()) {
455                        case Equals:
456                            if (primitive.getValueAsString().equals(matchingValue)) {
457                                matchingNodes.add(node);
458                            }
459                            break;
460                        case NotEquals:
461                            if (!primitive.getValueAsString().equals(matchingValue)) {
462                                matchingNodes.add(node);
463                            }
464                            break;
465                        case And:
466                        case As:
467                        case Concatenate:
468                        case Contains:
469                        case Div:
470                        case DivideBy:
471                        case Equivalent:
472                        case Greater:
473                        case GreaterOrEqual:
474                        case Implies:
475                        case In:
476                        case Is:
477                        case LessOrEqual:
478                        case LessThan:
479                        case MemberOf:
480                        case Minus:
481                        case Mod:
482                        case NotEquivalent:
483                        case Or:
484                        case Plus:
485                        case Times:
486                        case Union:
487                        case Xor:
488                            // TODO: unimplemented, what to do?
489                    }
490                }
491            }
492        }
493
494        if (matchingNodes.size() == 0) {
495            if (unlabeledNodes.size() == 0) {
496                // no nodes were matched and no unlabeled nodes are available. We need to add a
497                // sister node to the nodeStack
498                GenerationTier latestTier = this.nodeStack.pop();
499                GenerationTier previousTier = this.nodeStack.peek();
500                this.nodeStack.push(latestTier);
501
502                RuntimeCompositeDatatypeDefinition compositeTarget = (RuntimeCompositeDatatypeDefinition) latestTier.nodeDefinition;
503                ICompositeType compositeNode = compositeTarget
504                        .newInstance(latestTier.childDefinition.getInstanceConstructorArguments());
505                latestTier.childDefinition.getMutator().addValue(previousTier.nodes.get(0), compositeNode);
506                unlabeledNodes.add(compositeNode);
507            }
508
509            switch(param.getOperation()) {
510                case Equals:
511                    // if we are checking for equality, we need to set the property we looked for on
512                    // the unlabeled node(s)
513                    RuntimePrimitiveDatatypeDefinition equalsPrimitive = (RuntimePrimitiveDatatypeDefinition) wherePropertyDefinition;
514                    IPrimitiveType<?> primitive = equalsPrimitive
515                            .newInstance(wherePropertyChild.getInstanceConstructorArguments());
516                    primitive.setValueAsString(param.getOpNext().getConstant().toString());
517                    for (IBase node : unlabeledNodes) {
518                        wherePropertyChild.getMutator().addValue(node, primitive);
519                        matchingNodes.add(node);
520                    }
521                    break;
522                case NotEquals:
523                    // if we are checking for inequality, we need to pass all unlabeled (or created
524                    // if none were available)
525                    matchingNodes.addAll(unlabeledNodes);
526                    break;
527                case And:
528                case As:
529                case Concatenate:
530                case Contains:
531                case Div:
532                case DivideBy:
533                case Equivalent:
534                case Greater:
535                case GreaterOrEqual:
536                case Implies:
537                case In:
538                case Is:
539                case LessOrEqual:
540                case LessThan:
541                case MemberOf:
542                case Minus:
543                case Mod:
544                case NotEquivalent:
545                case Or:
546                case Plus:
547                case Times:
548                case Union:
549                case Xor:
550                    // TODO: need to implement above first
551            }
552        }
553
554        // set the nodes to the filtered ones
555        this.nodeStack.peek().nodes = matchingNodes;
556    }
557
558    /**
559     * Creates a list all FHIRPaths from the mapping ordered by paths with where
560     * equals, where unequals and the rest.
561     * 
562     * @return List<String> a List of FHIRPaths ordered by the type.
563     */
564    private List<String> sortedPaths() {
565        List<String> whereEquals = new ArrayList<String>();
566        List<String> whereUnequals = new ArrayList<String>();
567        List<String> withoutWhere = new ArrayList<String>();
568
569        for (String fhirPath : this.pathMapping.keySet()) {
570            switch (this.getTypeOfFhirPath(fhirPath)) {
571                case WHERE_EQUALS:
572                    whereEquals.add(fhirPath);
573                    break;
574                case WHERE_UNEQUALS:
575                    whereUnequals.add(fhirPath);
576                    break;
577                case WITHOUT_WHERE:
578                    withoutWhere.add(fhirPath);
579                    break;
580            }
581        }
582
583        List<String> ret = new ArrayList<String>();
584        ret.addAll(whereEquals);
585        ret.addAll(whereUnequals);
586        ret.addAll(withoutWhere);
587        return ret;
588    }
589
590    /**
591     * Returns the type of path based on the FHIRPath String.
592     * 
593     * @param fhirPath String representation of a FHIRPath.
594     * @return PathType the type of path supplied as `fhirPath`.
595     */
596    private PathType getTypeOfFhirPath(String fhirPath) {
597        ExpressionNode fhirPathExpression = this.engine.parse(fhirPath);
598        while (fhirPathExpression != null) {
599            if (fhirPathExpression.getKind() == ExpressionNode.Kind.Function) {
600                if (fhirPathExpression.getFunction() == ExpressionNode.Function.Where) {
601                    for (ExpressionNode params : fhirPathExpression.getParameters()) {
602                        switch (params.getOperation()) {
603                            case Equals:
604                                return PathType.WHERE_EQUALS;
605                            case NotEquals:
606                                return PathType.WHERE_UNEQUALS;
607                            case And:
608                            case As:
609                            case Concatenate:
610                            case Contains:
611                            case Div:
612                            case DivideBy:
613                            case Equivalent:
614                            case Greater:
615                            case GreaterOrEqual:
616                            case Implies:
617                            case In:
618                            case Is:
619                            case LessOrEqual:
620                            case LessThan:
621                            case MemberOf:
622                            case Minus:
623                            case Mod:
624                            case NotEquivalent:
625                            case Or:
626                            case Plus:
627                            case Times:
628                            case Union:
629                            case Xor:
630                                // TODO: need to implement above first
631                        }
632                    }
633                }
634            }
635            fhirPathExpression = fhirPathExpression.getInner();
636        }
637        return PathType.WITHOUT_WHERE;
638    }
639
640    /**
641     * A simple enum to diffirentiate between types of FHIRPaths in the special use
642     * case of generating FHIR Resources.
643     */
644    public enum PathType {
645        WHERE_EQUALS, WHERE_UNEQUALS, WITHOUT_WHERE
646    }
647}