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}