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