001/*-
002 * #%L
003 * HAPI FHIR Storage api
004 * %%
005 * Copyright (C) 2014 - 2025 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.jpa.patch;
021
022import ca.uhn.fhir.i18n.Msg;
023import ca.uhn.fhir.jpa.util.RandomTextUtils;
024import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
025import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
026
027import java.util.ArrayList;
028import java.util.List;
029import java.util.function.Predicate;
030
031import static net.sourceforge.plantuml.StringUtils.isNotEmpty;
032
033/**
034 * This class creates and contains a parsed fhirpath.
035 * -
036 * It does not *validate* said fhir path (it might not be a valid path at all).
037 * It is only used for parsing convenience.
038 */
039public class ParsedFhirPath {
040        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ParsedFhirPath.class);
041
042        public static class FhirPathNode {
043                /**
044                 * The node before this one;
045                 * if this is a head node, previous will be null
046                 */
047                private FhirPathNode myPrevious;
048
049                /**
050                 * The node after this one;
051                 * if this is a tail node, next will be null
052                 */
053                private FhirPathNode myNext;
054
055                /**
056                 * The value of this node.
057                 */
058                private final String myValue;
059
060                /**
061                 * If this node is a filter node (ie, ends with [x] where x is some
062                 * list index), this value will be an integer >= 0
063                 */
064                private int myListIndex = -1;
065
066                private boolean myIsValueNode = false;
067
068                public FhirPathNode(String theValue) {
069                        myValue = theValue;
070
071                        int open = theValue.indexOf("[");
072                        if (open != -1) {
073                                int close = RandomTextUtils.findMatchingClosingBrace(open, myValue, '[', ']');
074                                if (close != -1) {
075                                        String val = theValue.substring(open + 1, close);
076                                        try {
077                                                myListIndex = Integer.parseInt(val);
078                                        } catch (NumberFormatException ex) {
079                                                // TODO - not a number, but an expression
080                                                /*
081                                                 * Our current implementation does not account for
082                                                 * expressions, but only indices.
083                                                 * so we'll just note this for now
084                                                 */
085                                                ourLog.warn("{} is not an integer", val);
086                                        }
087                                }
088                        }
089                }
090
091                public String getValue() {
092                        return myValue;
093                }
094
095                /**
096                 * Returns true if this is a value node (ie, non-function, non-path node)
097                 */
098                public boolean isValueNode() {
099                        return myIsValueNode;
100                }
101
102                public void setAsValueNode(boolean theBool) {
103                        myIsValueNode = theBool;
104                }
105
106                public boolean hasListIndex() {
107                        return myListIndex >= 0;
108                }
109
110                public int getListIndex() {
111                        return myListIndex;
112                }
113
114                /**
115                 * If this node is actually a function node, this will return true.
116                 */
117                public boolean isFunction() {
118                        return isFilter();
119                }
120
121                /**
122                 * Filters are any of the "end function" or index
123                 * as defined by http://hl7.org/fhirpath/N1/#subsetting
124                 */
125                public boolean isFilter() {
126                        return myListIndex >= 0;
127                }
128
129                public boolean isNormalPathNode() {
130                        return !isFunction() && !isFilter() && !isValueNode() && !hasListIndex();
131                }
132
133                void setNext(FhirPathNode theNextNode) {
134                        myNext = theNextNode;
135                        if (theNextNode != null) {
136                                theNextNode.setPrevious(this);
137                        }
138                }
139
140                public FhirPathNode getNext() {
141                        return myNext;
142                }
143
144                void setPrevious(FhirPathNode thePreviousNode) {
145                        myPrevious = thePreviousNode;
146                }
147
148                public FhirPathNode getPrevious() {
149                        return myPrevious;
150                }
151
152                public boolean hasPrevious() {
153                        return myPrevious != null;
154                }
155
156                public boolean hasNext() {
157                        return myNext != null;
158                }
159        }
160
161        /**
162         * A fhirpath node that is actually a function
163         * see http://hl7.org/fhirpath/N1/#literals
164         */
165        public static class FhirPathFunction extends FhirPathNode {
166
167                /**
168                 * The contained expression is, itself, a fhir path
169                 * It could be a fhir path of a single element, but it is
170                 * a fhir path all its own
171                 */
172                private ParsedFhirPath myContainedExp;
173
174                // http://hl7.org/fhirpath/N1/#literals
175                public FhirPathFunction(String theValue) {
176                        super(theValue);
177                }
178
179                public void setContainedExpression(String theContainedExpression) {
180                        myContainedExp = ParsedFhirPath.parse(theContainedExpression);
181                }
182
183                /**
184                 * Whether or not this node contains a sub-fhir-path
185                 */
186                public boolean hasContainedExp() {
187                        return myContainedExp != null;
188                }
189
190                public ParsedFhirPath getContainedExp() {
191                        return myContainedExp;
192                }
193
194                @Override
195                public boolean isFunction() {
196                        return true;
197                }
198
199                @Override
200                public boolean isFilter() {
201                        return super.isFilter() || isFilteredFunction();
202                }
203
204                private boolean isFilteredFunction() {
205                        switch (getValue()) {
206                                case "first", "last", "tail", "skip", "take", "intersect", "exclude", "single" -> {
207                                        return true;
208                                }
209                        }
210                        return false;
211                }
212        }
213
214        /**
215         * The head node. Should never be null.
216         */
217        private final FhirPathNode myHead;
218
219        /**
220         * The tail node. Should never be null.
221         */
222        private FhirPathNode myTail;
223
224        /**
225         * The full path, unparsed, of this particular FhirPath
226         */
227        private final String myRawPath;
228
229        /**
230         * Whether or not this fhirpath ends with a filter.
231         * See http://hl7.org/fhirpath/N1/#filtering-and-projection for
232         * filter definitions
233         */
234        private boolean myEndsWithFilter;
235
236        /**
237         * Whether or not this fhirpath ends with an
238         * index (ie, a node with [n] where n is some number)
239         */
240        private boolean myEndsWithAnIndex;
241
242        ParsedFhirPath(String theFullPath) {
243                myRawPath = theFullPath;
244
245                myHead = createNode(this, theFullPath);
246        }
247
248        public String getRawPath() {
249                return myRawPath;
250        }
251
252        public FhirPathNode getHead() {
253                return myHead;
254        }
255
256        public void setTail(FhirPathNode theTail) {
257                myTail = theTail;
258        }
259
260        public FhirPathNode getTail() {
261                return myTail;
262        }
263
264        public void setEndsWithFilter(boolean theEndsWithFilter) {
265                myEndsWithFilter = theEndsWithFilter;
266        }
267
268        public boolean endsWithAFilter() {
269                return myEndsWithFilter;
270        }
271
272        public void setEndsWithAnIndex(boolean theEndsWithAnIndex) {
273                myEndsWithAnIndex = theEndsWithAnIndex;
274        }
275
276        public boolean endsWithAnIndex() {
277                return myEndsWithAnIndex;
278        }
279
280        public boolean endsWithFilterOrIndex() {
281                return endsWithAFilter() || endsWithAnIndex();
282        }
283
284        public FhirPathNode getFirstNode(Predicate<FhirPathNode> thePred) {
285                ParsedFhirPath.FhirPathNode node = getHead();
286                while (node != null) {
287                        if (thePred.test(node)) {
288                                return node;
289                        }
290                        if (node instanceof FhirPathFunction fn && fn.hasContainedExp()) {
291                                FhirPathNode subNode = fn.getContainedExp().getFirstNode(thePred);
292                                if (subNode != null) {
293                                        return subNode;
294                                }
295                        }
296                        node = node.getNext();
297                }
298                return null;
299        }
300
301        /**
302         * Returns the top level path from the provided from predicate to the provided to predicate
303         *
304         * Top level means it will not delve into subpaths.
305         * So, given the example Patient.name.where(given.startsWith('homer')).first()
306         * you cannot construct a path from "Patient" to "given"
307         * @param theFromPred the inclusive beginning predicate
308         * @param theToPred the exclusive ending predicate
309         * @return
310         */
311        public String getTopLevelPathFromTo(Predicate<FhirPathNode> theFromPred, Predicate<FhirPathNode> theToPred) {
312                FhirPathNode node = getHead();
313                StringBuilder sb = new StringBuilder();
314                boolean hasStarted = false;
315                while (node != null) {
316                        if (!hasStarted) {
317                                hasStarted = theFromPred.test(node);
318                        }
319
320                        if (hasStarted) {
321                                if (theToPred.test(node)) {
322                                        break;
323                                }
324
325                                String nextVal = node.getValue();
326                                if (!sb.isEmpty() && !nextVal.startsWith("[")) {
327                                        sb.append(".");
328                                }
329
330                                sb.append(nextVal);
331
332                                if (node instanceof FhirPathFunction fn) {
333                                        sb.append("(");
334
335                                        // it is invalid to request a subpath until a nested value
336                                        if (fn.hasContainedExp()) {
337                                                sb.append(fn.getContainedExp().getRawPath());
338                                        }
339
340                                        sb.append(")");
341                                }
342                        }
343                        node = node.getNext();
344                }
345                return sb.toString();
346        }
347
348        /**
349         * Returns the fhir path up to the last element.
350         * If there is a filter, it will return the element before the last
351         * (filtered) element.
352         */
353        public String getContainingPath() {
354                StringBuilder sb = new StringBuilder();
355                FhirPathNode current = myHead;
356                while (current != null && !current.isFunction()) {
357                        if (!sb.isEmpty()) {
358                                sb.append(".");
359                        }
360                        sb.append(current.getValue());
361
362                        current = current.getNext();
363                }
364                return sb.toString();
365        }
366
367        /**
368         * Returns all nodes, ordered, filtering by the provided predicate
369         */
370        public void getAllNodesWithPred(List<FhirPathNode> theNodes, Predicate<FhirPathNode> thePred) {
371                FhirPathNode node = getHead();
372                while (node != null) {
373                        if (thePred.test(node)) {
374                                theNodes.add(node);
375                        }
376
377                        if (node instanceof FhirPathFunction fn && fn.hasContainedExp()) {
378                                fn.getContainedExp().getAllNodesWithPred(theNodes, thePred);
379                        }
380                        node = node.getNext();
381                }
382        }
383
384        /**
385         * Returns the last non-function, non-value node
386         */
387        public FhirPathNode getFinalPathNode() {
388                FhirPathNode node = myTail;
389                while (node != null) {
390                        if (node.isFunction()) {
391                                if (node.hasListIndex()) {
392                                        node = node.getPrevious();
393                                } else {
394                                        FhirPathFunction fn = (FhirPathFunction) node;
395                                        if (!fn.hasContainedExp()) {
396                                                node = node.getPrevious();
397                                        } else {
398                                                // recurse
399                                                FhirPathNode subNode = fn.getContainedExp().getFinalPathNode();
400                                                if (subNode == null || subNode.isValueNode()) {
401                                                        node = node.getPrevious();
402                                                } else {
403                                                        return subNode;
404                                                }
405                                        }
406                                }
407                        } else if (!node.isValueNode()) {
408                                return node;
409                        } else {
410                                node = node.getPrevious();
411                        }
412                }
413
414                return null;
415        }
416
417        /**
418         * Returns the final non-function node value in the expression.
419         * Eg:
420         * Patient.name.given -> returns "given"
421         * Patient.name.where(given.startsWith("John")) -> returns "given"
422         */
423        public String getLastElementName() {
424                FhirPathNode finalNode = getFinalPathNode();
425                if (finalNode == null) {
426                        ourLog.error("No path nodes in fhirpath");
427                        return null;
428                }
429                return finalNode.getValue();
430        }
431
432        /**
433         * Retrieve a list of nodes that meet the given predicate.
434         * These nodes may or may not be siblings, but it will not
435         * include children of nodes (ie, only top-level nodes).
436         */
437        public List<FhirPathNode> getNodes(Predicate<FhirPathNode> thePred) {
438                List<FhirPathNode> nodes = new ArrayList<>();
439                FhirPathNode node = getHead();
440
441                while (node != null) {
442                        if (thePred.test(node)) {
443                                nodes.add(node);
444                        }
445                        node = node.getNext();
446                }
447
448                return nodes;
449        }
450
451        /**
452         * Create a ParsedFhirPath from a raw string.
453         */
454        public static ParsedFhirPath parse(String theFullPath) {
455                return new ParsedFhirPath(theFullPath);
456        }
457
458        /**
459         * Create a FhirPathNode, recursively constructing all
460         * it's neighbours and/or children (if it's a function).
461         */
462        private static FhirPathNode createNode(ParsedFhirPath theParsedFhirPath, String thePath) {
463                int dotIndex = thePath.indexOf(".");
464                int braceIndex = thePath.indexOf("(");
465
466                FhirPathNode next;
467                if (dotIndex == -1) {
468                        int filterIndex = thePath.indexOf("[");
469                        FhirPathNode tail;
470                        boolean endsInFilter = false;
471                        // base cases
472                        if (braceIndex == -1 && filterIndex == -1) {
473                                // base case 1 - a standard node (no braces () or dots . or filters [])
474                                // ending is just a path element
475                                next = new FhirPathNode(thePath);
476                                if (!next.isFilter() && !next.isFunction() && isValueNode(thePath)) {
477                                        next.setAsValueNode(true);
478                                }
479                                tail = next;
480                        } else if (filterIndex == -1) {
481                                // base case 2 - a filter function (function ending in ())
482                                // ending is a function
483                                String funcType = thePath.substring(0, braceIndex);
484                                // -1 because we don't care about the last bracket
485                                String containedExp = thePath.substring(braceIndex + 1, thePath.length() - 1);
486                                // the function has parameters -> a contained fhirpath
487                                next = new FhirPathFunction(funcType);
488                                if (isNotEmpty(containedExp)) {
489                                        ((FhirPathFunction) next).setContainedExpression(containedExp);
490                                }
491
492                                // TODO - this is not technically correct
493                                /*
494                                 * Our current implementations do not distinguish between
495                                 * "filter" and "function", so we'll treat all functions as
496                                 * filters
497                                 */
498                                endsInFilter = true;
499                                tail = next;
500                        } else {
501                                // base case 3 - path contains a filter ([]).. and potentially a functions
502                                // ie, either path[n] or path()[n]
503                                int closingFilter = RandomTextUtils.findMatchingClosingBrace(filterIndex, thePath, '[', ']');
504                                endsInFilter = true;
505
506                                // part1 -> part before the opening filter brace [
507                                String part1 = thePath.substring(0, filterIndex);
508
509                                // the filter value (a number)
510                                String part2 = thePath.substring(filterIndex, closingFilter + 1);
511
512                                // there are parts of a fhirpath past the [] filter
513                                String part3 = thePath.substring(closingFilter + 1);
514
515                                // create part2 first - the filter node
516                                ParsedFhirPath.FhirPathNode filterNode = new ParsedFhirPath.FhirPathNode(part2);
517
518                                // if there's a part1 - create the first node
519                                if (isNotEmpty(part1)) {
520                                        // create node chain for first part;
521                                        ParsedFhirPath.FhirPathNode p1node = createNode(theParsedFhirPath, part1);
522
523                                        ParsedFhirPath.FhirPathNode node = p1node;
524                                        while (node.getNext() != null) {
525                                                node = node.getNext();
526                                        }
527                                        // set the filter node as the last in the chain
528                                        node.setNext(filterNode);
529                                        // and the first in the chain as the node to return
530                                        next = p1node;
531                                } else {
532                                        // otherwise, our next node is the filter node
533                                        // (ie, there is no "first part")
534                                        next = filterNode;
535                                }
536
537                                // part3 - the part after the closing filter brace ]
538                                if (isNotEmpty(part3)) {
539                                        throw new InvalidRequestException(
540                                                        Msg.code(2713) + " Unexpected path after filter: " + thePath.substring(closingFilter + 1));
541                                } else {
542                                        // the filter is the end node; nothing more to add
543                                        tail = filterNode;
544                                }
545                                theParsedFhirPath.setEndsWithAnIndex(true);
546                        }
547                        theParsedFhirPath.setEndsWithFilter(endsInFilter);
548
549                        theParsedFhirPath.setTail(tail);
550                } else if (braceIndex != -1 && braceIndex < dotIndex) {
551                        // recursive case 1
552                        // next part is a function
553                        // could contain an expression or singular element
554                        int closingIndex = RandomTextUtils.findMatchingClosingBrace(braceIndex, thePath);
555
556                        if (closingIndex == -1) {
557                                String msg = String.format("Path %s contains an unmatched brace at %d", thePath, braceIndex);
558                                ourLog.error(msg);
559
560                                throw new InternalErrorException(Msg.code(2714) + " " + msg);
561                        }
562
563                        String funcType = thePath.substring(0, braceIndex);
564                        String containedExp = thePath.substring(braceIndex + 1, closingIndex);
565
566                        // an additional +1 for closing index, since
567                        int remainingIndex = dotIndex;
568                        if (closingIndex > dotIndex) {
569                                // we want the first dot after the closing brace
570                                remainingIndex = thePath.indexOf(".", closingIndex);
571
572                                if (remainingIndex == -1) {
573                                        // there is no . after the closing brace...
574                                        remainingIndex = closingIndex;
575                                }
576                        }
577                        String remaining = thePath.substring(remainingIndex + 1);
578
579                        FhirPathFunction func = new FhirPathFunction(funcType);
580                        if (isNotEmpty(containedExp)) {
581                                // function contains an expression; set it here
582                                func.setContainedExpression(containedExp);
583                        }
584                        next = func;
585
586                        if (isNotEmpty(remaining)) {
587                                next.setNext(createNode(theParsedFhirPath, remaining));
588                        } else {
589                                theParsedFhirPath.setTail(next);
590                        }
591
592                        // not technically true; but our code treats anything with a function
593                        // as having a filter
594                        theParsedFhirPath.setEndsWithFilter(true);
595                } else {
596                        // recursive case 2
597                        int filterIndex = thePath.indexOf("[");
598
599                        if (filterIndex == -1 || filterIndex > dotIndex) {
600                                // next element is a standard node element (not a function)
601                                String nextPart = thePath.substring(0, dotIndex);
602                                String remaining = thePath.substring(dotIndex + 1);
603
604                                next = new FhirPathNode(nextPart);
605                                next.setNext(createNode(theParsedFhirPath, remaining));
606                        } else {
607                                // next element has a filter on it
608                                int closingFilter = RandomTextUtils.findMatchingClosingBrace(filterIndex, thePath, '[', ']');
609
610                                String nextPart = thePath.substring(0, filterIndex);
611                                next = new FhirPathNode(nextPart);
612
613                                // the filter value (a number)
614                                String filterPart = thePath.substring(filterIndex, closingFilter + 1);
615                                FhirPathNode filterNode = new FhirPathNode(filterPart);
616                                next.setNext(filterNode);
617
618                                String remaining = thePath.substring(dotIndex + 1);
619                                filterNode.setNext(createNode(theParsedFhirPath, remaining));
620                        }
621                }
622
623                return next;
624        }
625
626        private static boolean isValueNode(String theValue) {
627                if (theValue.contains("'")) {
628                        return true;
629                }
630                try {
631                        Integer.parseInt(theValue);
632                        return true;
633                } catch (NumberFormatException ex) {
634                        return false;
635                }
636        }
637}