001/*-
002 * #%L
003 * HAPI FHIR JPA Server
004 * %%
005 * Copyright (C) 2014 - 2024 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.dao.predicate;
021
022import ca.uhn.fhir.i18n.Msg;
023import org.apache.commons.lang3.StringUtils;
024
025import java.util.Arrays;
026import java.util.List;
027import java.util.regex.Matcher;
028import java.util.regex.Pattern;
029
030public class SearchFilterParser {
031
032        private static final String XML_DATE_PATTERN =
033                        "[0-9]{4}(-(0[1-9]|1[0-2])(-(0[0-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|([+\\-])((0[0-9]|1[0-3]):[0-5][0-9]|14:00))?)?)?)?";
034        private static final Pattern XML_DATE_MATCHER = Pattern.compile(XML_DATE_PATTERN);
035        private static final List<String> CODES_CompareOperation =
036                        Arrays.asList("eq", "ne", "co", "sw", "ew", "gt", "lt", "ge", "le", "pr", "po", "ss", "sb", "in", "re");
037        private static final List<String> CODES_LogicalOperation = Arrays.asList("and", "or", "not");
038        private String original = null;
039        private int cursor;
040
041        private boolean isDate(String s) {
042                Matcher m = XML_DATE_MATCHER.matcher(s);
043                return m.matches();
044        }
045
046        private FilterLexType peek() throws FilterSyntaxException {
047
048                FilterLexType result;
049                while ((cursor < original.length()) && (original.charAt(cursor) == ' ')) {
050                        cursor++;
051                }
052
053                if (cursor >= original.length()) {
054                        result = FilterLexType.fsltEnded;
055                } else {
056                        if (((original.charAt(cursor) >= 'a') && (original.charAt(cursor) <= 'z'))
057                                        || ((original.charAt(cursor) >= 'A') && (original.charAt(cursor) <= 'Z'))
058                                        || (original.charAt(cursor) == '_')) {
059                                result = FilterLexType.fsltName;
060                        } else if ((original.charAt(cursor) >= '0') && (original.charAt(cursor) <= '9')) {
061                                result = FilterLexType.fsltNumber;
062                        } else if (original.charAt(cursor) == '"') {
063                                result = FilterLexType.fsltString;
064                        } else if (original.charAt(cursor) == '.') {
065                                result = FilterLexType.fsltDot;
066                        } else if (original.charAt(cursor) == '(') {
067                                result = FilterLexType.fsltOpen;
068                        } else if (original.charAt(cursor) == ')') {
069                                result = FilterLexType.fsltClose;
070                        } else if (original.charAt(cursor) == '[') {
071                                result = FilterLexType.fsltOpenSq;
072                        } else if (original.charAt(cursor) == ']') {
073                                result = FilterLexType.fsltCloseSq;
074                        } else {
075                                throw new FilterSyntaxException(
076                                                Msg.code(1052) + String.format("Unknown Character \"%s\" at %d", peekCh(), cursor));
077                        }
078                }
079                return result;
080        }
081
082        private String peekCh() {
083
084                String result;
085                if (cursor > original.length()) {
086                        result = "[end!]";
087                } else {
088                        result = original.substring(cursor, cursor + 1);
089                }
090                return result;
091        }
092
093        private String consumeName() {
094
095                String result;
096                int i = cursor;
097                do {
098                        i++;
099                } while ((i <= original.length() - 1)
100                                && (((original.charAt(i) >= 'a') && (original.charAt(i) <= 'z'))
101                                                || ((original.charAt(i) >= 'A') && (original.charAt(i) <= 'Z'))
102                                                || ((original.charAt(i) >= '0') && (original.charAt(i) <= '9'))
103                                                || (original.charAt(i) == '-')
104                                                || (original.charAt(i) == '_')
105                                                || (original.charAt(i) == ':')));
106
107                result = original.substring(cursor, i /* - cursor*/);
108                cursor = i;
109                return result;
110        }
111
112        private String consumeToken() {
113
114                String result;
115                int i = cursor;
116                do {
117                        i++;
118                } while ((i <= original.length() - 1)
119                                && (original.charAt(i) > 32)
120                                && (!StringUtils.isWhitespace(original.substring(i, i + 1)))
121                                && (original.charAt(i) != ')')
122                                && (original.charAt(i) != ']'));
123                result = original.substring(cursor, i /* - cursor*/);
124                cursor = i;
125                return result;
126        }
127
128        private String consumeNumberOrDate() {
129
130                String result;
131                int i = cursor;
132                do {
133                        i++;
134                } while ((i <= original.length() - 1)
135                                && (((original.charAt(i) >= '0') && (original.charAt(i) <= '9'))
136                                                || (original.charAt(i) == '.')
137                                                || (original.charAt(i) == '-')
138                                                || (original.charAt(i) == ':')
139                                                || (original.charAt(i) == '+')
140                                                || (original.charAt(i) == 'T')));
141                result = original.substring(cursor, i /* - cursor*/);
142                cursor = i;
143                return result;
144        }
145
146        private String consumeString() throws FilterSyntaxException {
147
148                //                      int l = 0;
149                cursor++;
150                StringBuilder str = new StringBuilder(original.length());
151                //                      setLength(result, length(original)); // can't be longer than that
152                while ((cursor <= original.length()) && (original.charAt(cursor) != '"')) {
153                        //                              l++;
154                        if (original.charAt(cursor) != '\\') {
155                                str.append(original.charAt(cursor));
156                                //                                      str.setCharAt(l, original.charAt(cursor));
157                        } else {
158                                cursor++;
159                                if (original.charAt(cursor) == '"') {
160                                        str.append('"');
161                                        //                                              str.setCharAt(l, '"');
162                                } else if (original.charAt(cursor) == 't') {
163                                        str.append('\t');
164                                        //                                              str.setCharAt(l, '\t');
165                                } else if (original.charAt(cursor) == 'r') {
166                                        str.append('\r');
167                                        //                                              str.setCharAt(l, '\r');
168                                } else if (original.charAt(cursor) == 'n') {
169                                        str.append('\n');
170                                        //                                              str.setCharAt(l, '\n');
171                                } else {
172                                        throw new FilterSyntaxException(
173                                                        Msg.code(1053) + String.format("Unknown escape sequence at %d", cursor));
174                                }
175                        }
176                        cursor++;
177                }
178                //                      SetLength(result, l);
179                if ((cursor > original.length()) || (original.charAt(cursor) != '"')) {
180                        throw new FilterSyntaxException(
181                                        Msg.code(1054) + String.format("Problem with string termination at %d", cursor));
182                }
183
184                if (str.length() == 0) {
185                        throw new FilterSyntaxException(
186                                        Msg.code(1055) + String.format("Problem with string at %d cannot be empty", cursor));
187                }
188
189                cursor++;
190                return str.toString();
191        }
192
193        private BaseFilter parse() throws FilterSyntaxException {
194
195                BaseFilter result = parseOpen();
196                if (cursor < original.length()) {
197                        throw new FilterSyntaxException(
198                                        Msg.code(1056) + String.format("Expression did not terminate at %d", cursor));
199                }
200                return result;
201        }
202
203        private BaseFilter parseOpen() throws FilterSyntaxException {
204
205                BaseFilter result;
206                String s;
207                FilterParameterGroup grp;
208                if (peek() == FilterLexType.fsltOpen) {
209                        cursor++;
210                        grp = new FilterParameterGroup();
211                        grp.setContained(parseOpen());
212                        if (peek() != FilterLexType.fsltClose) {
213                                throw new FilterSyntaxException(
214                                                Msg.code(1057) + String.format("Expected ')' at %d but found %s", cursor, peekCh()));
215                        }
216                        cursor++;
217                        FilterLexType lexType = peek();
218                        if (lexType == FilterLexType.fsltName) {
219                                result = parseLogical(grp);
220                        } else if ((lexType == FilterLexType.fsltEnded)
221                                        || (lexType == FilterLexType.fsltClose)
222                                        || (lexType == FilterLexType.fsltCloseSq)) {
223                                result = grp;
224                        } else {
225                                throw new FilterSyntaxException(
226                                                Msg.code(1058) + String.format("Unexpected Character %s at %d", peekCh(), cursor));
227                        }
228                } else {
229                        s = consumeName();
230                        if (s.compareToIgnoreCase("not") == 0) {
231                                result = parseLogical(null);
232                        } else {
233                                result = parseParameter(s);
234                        }
235                }
236                return result;
237        }
238
239        private BaseFilter parseLogical(BaseFilter filter) throws FilterSyntaxException {
240
241                BaseFilter result = null;
242                String s;
243                FilterLogical logical;
244                if (filter == null) {
245                        s = "not";
246                } else {
247                        s = consumeName();
248                        if ((!s.equals("or")) && (!s.equals("and")) && (!s.equals("not"))) {
249                                throw new FilterSyntaxException(Msg.code(1059) + String.format("Unexpected Name %s at %d", s, cursor));
250                        }
251
252                        logical = new FilterLogical();
253                        logical.setFilter1(filter);
254                        if (s.compareToIgnoreCase("or") == 0) {
255                                logical.setOperation(FilterLogicalOperation.or);
256                        } else if (s.compareToIgnoreCase("not") == 0) {
257                                logical.setOperation(FilterLogicalOperation.not);
258                        } else {
259                                logical.setOperation(FilterLogicalOperation.and);
260                        }
261
262                        logical.setFilter2(parseOpen());
263                        result = logical;
264                }
265                return result;
266        }
267
268        private FilterParameterPath parsePath(String name) throws FilterSyntaxException {
269
270                FilterParameterPath result = new FilterParameterPath();
271                result.setName(name);
272                if (peek() == FilterLexType.fsltOpenSq) {
273                        cursor++;
274                        result.setFilter(parseOpen());
275                        if (peek() != FilterLexType.fsltCloseSq) {
276                                throw new FilterSyntaxException(
277                                                Msg.code(1060) + String.format("Expected ']' at %d but found %s", cursor, peekCh()));
278                        }
279                        cursor++;
280                }
281
282                if (peek() == FilterLexType.fsltDot) {
283                        cursor++;
284                        if (peek() != FilterLexType.fsltName) {
285                                throw new FilterSyntaxException(
286                                                Msg.code(1061) + String.format("Unexpected Character %s at %d", peekCh(), cursor));
287                        }
288                        result.setNext(parsePath(consumeName()));
289                } else if (result.getFilter() != null) {
290                        throw new FilterSyntaxException(
291                                        Msg.code(1062) + String.format("Expected '.' at %d but found %s", cursor, peekCh()));
292                }
293
294                return result;
295        }
296
297        private BaseFilter parseParameter(String name) throws FilterSyntaxException {
298
299                BaseFilter result;
300                String s;
301                FilterParameter filter = new FilterParameter();
302
303                // 1. the path
304                filter.setParamPath(parsePath(name));
305
306                if (peek() != FilterLexType.fsltName) {
307                        throw new FilterSyntaxException(
308                                        Msg.code(1063) + String.format("Unexpected Character %s at %d", peekCh(), cursor));
309                }
310                s = consumeName();
311                int index = CODES_CompareOperation.indexOf(s);
312                if (index == -1) {
313                        throw new FilterSyntaxException(Msg.code(1064) + String.format("Unknown operation %s at %d", s, cursor));
314                }
315                filter.setOperation(CompareOperation.values()[index]);
316
317                FilterLexType lexType = peek();
318                if (lexType == FilterLexType.fsltName) {
319                        filter.setValue(consumeToken());
320                        filter.setValueType(FilterValueType.token);
321                } else if (lexType == FilterLexType.fsltNumber) {
322                        filter.setValue(consumeNumberOrDate());
323                        filter.setValueType(FilterValueType.numberOrDate);
324                } else if (lexType == FilterLexType.fsltString) {
325                        filter.setValue(consumeString());
326                        filter.setValueType(FilterValueType.string);
327                } else {
328                        throw new FilterSyntaxException(
329                                        Msg.code(1065) + String.format("Unexpected Character %s at %d", peekCh(), cursor));
330                }
331
332                // check operation / value type results
333                if (filter.getOperation() == CompareOperation.pr) {
334                        if ((filter.getValue().compareToIgnoreCase("true") != 0)
335                                        && (filter.getValue().compareToIgnoreCase("false") != 0)) {
336                                throw new FilterSyntaxException(Msg.code(1066)
337                                                + String.format(
338                                                                "Value %s not valid for operation %s at %d",
339                                                                filter.getValue(),
340                                                                CODES_CompareOperation.get(filter.getOperation().ordinal()),
341                                                                cursor));
342                        }
343                } else if (filter.getOperation() == CompareOperation.po) {
344                        if (!isDate(filter.getValue())) {
345                                throw new FilterSyntaxException(Msg.code(1067)
346                                                + String.format(
347                                                                "Value %s not valid for operation %s at %d",
348                                                                filter.getValue(),
349                                                                CODES_CompareOperation.get(filter.getOperation().ordinal()),
350                                                                cursor));
351                        }
352                }
353
354                lexType = peek();
355                if (lexType == FilterLexType.fsltName) {
356                        result = parseLogical(filter);
357                } else if ((lexType == FilterLexType.fsltEnded)
358                                || (lexType == FilterLexType.fsltClose)
359                                || (lexType == FilterLexType.fsltCloseSq)) {
360                        result = filter;
361                } else {
362                        throw new FilterSyntaxException(
363                                        Msg.code(1068) + String.format("Unexpected Character %s at %d", peekCh(), cursor));
364                }
365                return result;
366        }
367
368        public enum CompareOperation {
369                eq,
370                ne,
371                co,
372                sw,
373                ew,
374                gt,
375                lt,
376                ge,
377                le,
378                pr,
379                po,
380                ss,
381                sb,
382                in,
383                re,
384                ap,
385                sa,
386                eb
387        }
388
389        public enum FilterLogicalOperation {
390                and,
391                or,
392                not
393        }
394
395        public enum FilterItemType {
396                parameter,
397                logical,
398                parameterGroup
399        }
400
401        public enum FilterValueType {
402                token,
403                string,
404                numberOrDate
405        }
406
407        public enum FilterLexType {
408                fsltEnded,
409                fsltName,
410                fsltString,
411                fsltNumber,
412                fsltDot,
413                fsltOpen,
414                fsltClose,
415                fsltOpenSq,
416                fsltCloseSq
417        }
418
419        public abstract static class BaseFilter {
420
421                private FilterItemType itemType;
422
423                public FilterItemType getFilterItemType() {
424                        return itemType;
425                }
426        }
427
428        public static class FilterParameterPath {
429
430                private String FName;
431                private BaseFilter FFilter;
432                private FilterParameterPath FNext;
433
434                public String getName() {
435
436                        return FName;
437                }
438
439                public void setName(String value) {
440
441                        FName = value;
442                }
443
444                public BaseFilter getFilter() {
445
446                        return FFilter;
447                }
448
449                public void setFilter(BaseFilter value) {
450
451                        FFilter = value;
452                }
453
454                public FilterParameterPath getNext() {
455
456                        return FNext;
457                }
458
459                public void setNext(FilterParameterPath value) {
460
461                        FNext = value;
462                }
463
464                @Override
465                public String toString() {
466                        String result;
467                        if (getFilter() != null) {
468                                result = getName() + "[" + getFilter().toString() + "]";
469                        } else {
470                                result = getName();
471                        }
472                        if (getNext() != null) {
473                                result += "." + getNext().toString();
474                        }
475                        return result;
476                }
477        }
478
479        public static class FilterParameterGroup extends BaseFilter {
480
481                private BaseFilter FContained;
482
483                public BaseFilter getContained() {
484
485                        return FContained;
486                }
487
488                public void setContained(BaseFilter value) {
489
490                        FContained = value;
491                }
492
493                @Override
494                public String toString() {
495
496                        return "(" + FContained.toString() + ")";
497                }
498        }
499
500        public static class FilterParameter extends BaseFilter {
501
502                private FilterParameterPath FParamPath;
503                private CompareOperation FOperation;
504                private String FValue;
505                private FilterValueType FValueType;
506
507                public FilterParameterPath getParamPath() {
508
509                        return FParamPath;
510                }
511
512                void setParamPath(FilterParameterPath value) {
513
514                        FParamPath = value;
515                }
516
517                public CompareOperation getOperation() {
518
519                        return FOperation;
520                }
521
522                public void setOperation(CompareOperation value) {
523
524                        FOperation = value;
525                }
526
527                public String getValue() {
528
529                        return FValue;
530                }
531
532                public void setValue(String value) {
533
534                        FValue = value;
535                }
536
537                public FilterValueType getValueType() {
538
539                        return FValueType;
540                }
541
542                void setValueType(FilterValueType FValueType) {
543
544                        this.FValueType = FValueType;
545                }
546
547                @Override
548                public String toString() {
549                        if (FValueType == FilterValueType.string) {
550                                return getParamPath().toString() + " "
551                                                + CODES_CompareOperation.get(getOperation().ordinal()) + " \"" + getValue() + "\"";
552                        } else {
553                                return getParamPath().toString() + " "
554                                                + CODES_CompareOperation.get(getOperation().ordinal()) + " " + getValue();
555                        }
556                }
557        }
558
559        public static class FilterLogical extends BaseFilter {
560
561                private BaseFilter FFilter1;
562                private FilterLogicalOperation FOperation;
563                private BaseFilter FFilter2;
564
565                public BaseFilter getFilter1() {
566
567                        return FFilter1;
568                }
569
570                void setFilter1(BaseFilter FFilter1) {
571
572                        this.FFilter1 = FFilter1;
573                }
574
575                public FilterLogicalOperation getOperation() {
576
577                        return FOperation;
578                }
579
580                public void setOperation(FilterLogicalOperation FOperation) {
581
582                        this.FOperation = FOperation;
583                }
584
585                public BaseFilter getFilter2() {
586
587                        return FFilter2;
588                }
589
590                void setFilter2(BaseFilter FFilter2) {
591
592                        this.FFilter2 = FFilter2;
593                }
594
595                @Override
596                public String toString() {
597                        return FFilter1.toString() + " "
598                                        + CODES_LogicalOperation.get(getOperation().ordinal()) + " " + FFilter2.toString();
599                }
600        }
601
602        public static class FilterSyntaxException extends Exception {
603                FilterSyntaxException(String theMessage) {
604                        super(theMessage);
605                }
606        }
607
608        public static BaseFilter parse(String expression) throws FilterSyntaxException {
609                SearchFilterParser parser = new SearchFilterParser();
610                parser.original = expression;
611                parser.cursor = 0;
612                return parser.parse();
613        }
614}