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