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