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