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