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.hapi.ctx.HapiWorkerContext;
012import org.hl7.fhir.r4.model.ExpressionNode;
013import org.hl7.fhir.r4.model.Resource;
014import org.hl7.fhir.r4.utils.FHIRPathEngine;
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 <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<String, String> 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<String, String> 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 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 =
408                                        this.nodeStack.peek().nodeDefinition.getChildByName(param.getName());
409                        BaseRuntimeElementDefinition<?> wherePropertyDefinition =
410                                        wherePropertyChild.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 =
440                                this.nodeStack.peek().nodeDefinition.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 =
503                                                (RuntimeCompositeDatatypeDefinition) latestTier.nodeDefinition;
504                                ICompositeType compositeNode =
505                                                compositeTarget.newInstance(latestTier.childDefinition.getInstanceConstructorArguments());
506                                latestTier.childDefinition.getMutator().addValue(previousTier.nodes.get(0), compositeNode);
507                                unlabeledNodes.add(compositeNode);
508                        }
509
510                        switch (param.getOperation()) {
511                                case Equals:
512                                        // if we are checking for equality, we need to set the property we looked for on
513                                        // the unlabeled node(s)
514                                        RuntimePrimitiveDatatypeDefinition equalsPrimitive =
515                                                        (RuntimePrimitiveDatatypeDefinition) wherePropertyDefinition;
516                                        IPrimitiveType<?> primitive =
517                                                        equalsPrimitive.newInstance(wherePropertyChild.getInstanceConstructorArguments());
518                                        primitive.setValueAsString(param.getOpNext().getConstant().toString());
519                                        for (IBase node : unlabeledNodes) {
520                                                wherePropertyChild.getMutator().addValue(node, primitive);
521                                                matchingNodes.add(node);
522                                        }
523                                        break;
524                                case NotEquals:
525                                        // if we are checking for inequality, we need to pass all unlabeled (or created
526                                        // if none were available)
527                                        matchingNodes.addAll(unlabeledNodes);
528                                        break;
529                                case And:
530                                case As:
531                                case Concatenate:
532                                case Contains:
533                                case Div:
534                                case DivideBy:
535                                case Equivalent:
536                                case Greater:
537                                case GreaterOrEqual:
538                                case Implies:
539                                case In:
540                                case Is:
541                                case LessOrEqual:
542                                case LessThan:
543                                case MemberOf:
544                                case Minus:
545                                case Mod:
546                                case NotEquivalent:
547                                case Or:
548                                case Plus:
549                                case Times:
550                                case Union:
551                                case Xor:
552                                        // TODO: need to implement above first
553                        }
554                }
555
556                // set the nodes to the filtered ones
557                this.nodeStack.peek().nodes = matchingNodes;
558        }
559
560        /**
561         * Creates a list all FHIRPaths from the mapping ordered by paths with where
562         * equals, where unequals and the rest.
563         *
564         * @return List<String> a List of FHIRPaths ordered by the type.
565         */
566        private List<String> sortedPaths() {
567                List<String> whereEquals = new ArrayList<String>();
568                List<String> whereUnequals = new ArrayList<String>();
569                List<String> withoutWhere = new ArrayList<String>();
570
571                for (String fhirPath : this.pathMapping.keySet()) {
572                        switch (this.getTypeOfFhirPath(fhirPath)) {
573                                case WHERE_EQUALS:
574                                        whereEquals.add(fhirPath);
575                                        break;
576                                case WHERE_UNEQUALS:
577                                        whereUnequals.add(fhirPath);
578                                        break;
579                                case WITHOUT_WHERE:
580                                        withoutWhere.add(fhirPath);
581                                        break;
582                        }
583                }
584
585                List<String> ret = new ArrayList<String>();
586                ret.addAll(whereEquals);
587                ret.addAll(whereUnequals);
588                ret.addAll(withoutWhere);
589                return ret;
590        }
591
592        /**
593         * Returns the type of path based on the FHIRPath String.
594         *
595         * @param fhirPath String representation of a FHIRPath.
596         * @return PathType the type of path supplied as `fhirPath`.
597         */
598        private PathType getTypeOfFhirPath(String fhirPath) {
599                ExpressionNode fhirPathExpression = this.engine.parse(fhirPath);
600                while (fhirPathExpression != null) {
601                        if (fhirPathExpression.getKind() == ExpressionNode.Kind.Function) {
602                                if (fhirPathExpression.getFunction() == ExpressionNode.Function.Where) {
603                                        for (ExpressionNode params : fhirPathExpression.getParameters()) {
604                                                switch (params.getOperation()) {
605                                                        case Equals:
606                                                                return PathType.WHERE_EQUALS;
607                                                        case NotEquals:
608                                                                return PathType.WHERE_UNEQUALS;
609                                                        case And:
610                                                        case As:
611                                                        case Concatenate:
612                                                        case Contains:
613                                                        case Div:
614                                                        case DivideBy:
615                                                        case Equivalent:
616                                                        case Greater:
617                                                        case GreaterOrEqual:
618                                                        case Implies:
619                                                        case In:
620                                                        case Is:
621                                                        case LessOrEqual:
622                                                        case LessThan:
623                                                        case MemberOf:
624                                                        case Minus:
625                                                        case Mod:
626                                                        case NotEquivalent:
627                                                        case Or:
628                                                        case Plus:
629                                                        case Times:
630                                                        case Union:
631                                                        case Xor:
632                                                                // TODO: need to implement above first
633                                                }
634                                        }
635                                }
636                        }
637                        fhirPathExpression = fhirPathExpression.getInner();
638                }
639                return PathType.WITHOUT_WHERE;
640        }
641
642        /**
643         * A simple enum to diffirentiate between types of FHIRPaths in the special use
644         * case of generating FHIR Resources.
645         */
646        public enum PathType {
647                WHERE_EQUALS,
648                WHERE_UNEQUALS,
649                WITHOUT_WHERE
650        }
651}