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