
001package org.hl7.fhir.common.hapi.validation.validator; 002 003import ca.uhn.fhir.context.BaseRuntimeChildDefinition; 004import ca.uhn.fhir.context.BaseRuntimeElementDefinition; 005import ca.uhn.fhir.context.FhirContext; 006import ca.uhn.fhir.context.RuntimeCompositeDatatypeDefinition; 007import ca.uhn.fhir.context.RuntimePrimitiveDatatypeDefinition; 008import org.hl7.fhir.instance.model.api.IBase; 009import org.hl7.fhir.instance.model.api.ICompositeType; 010import org.hl7.fhir.instance.model.api.IPrimitiveType; 011import org.hl7.fhir.r4.fhirpath.ExpressionNode; 012import org.hl7.fhir.r4.fhirpath.FHIRPathEngine; 013import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext; 014import org.hl7.fhir.r4.model.Resource; 015 016import java.util.ArrayList; 017import java.util.HashMap; 018import java.util.List; 019import java.util.Map; 020import java.util.Stack; 021 022/** 023 * This class can be used to generate resources using FHIRPath expressions. 024 * 025 * Note that this is an experimental feature and the API is expected to change. Ideally 026 * this will be made version independent and moved out of the validation module 027 * in a future release. 028 * 029 * @author Marcel Parciak {@code <marcel.parciak@med.uni-goettingen.de>} 030 */ 031public class FHIRPathResourceGeneratorR4<T extends Resource> { 032 033 private FhirContext ctx; 034 private FHIRPathEngine engine; 035 private Map<String, String> pathMapping; 036 private T resource = null; 037 038 private String valueToSet = null; 039 private Stack<GenerationTier> nodeStack = null; 040 041 /** 042 * The GenerationTier summarizes some variables that are needed to create FHIR 043 * elements later on. 044 */ 045 class GenerationTier { 046 // The RuntimeDefinition of nodes 047 public BaseRuntimeElementDefinition<?> nodeDefinition = null; 048 // The actual nodes, i.e. the instances that hold the values 049 public List<IBase> nodes = new ArrayList<>(); 050 // The ChildDefinition applied to the parent (i.e. one of the nodes from a lower 051 // GenerationTier) to create nodes 052 public BaseRuntimeChildDefinition childDefinition = null; 053 // The path segment name of nodes 054 public String fhirPathName = null; 055 056 public GenerationTier() {} 057 058 public GenerationTier(BaseRuntimeElementDefinition<?> nodeDef, IBase firstNode) { 059 this.nodeDefinition = nodeDef; 060 this.nodes.add(firstNode); 061 } 062 } 063 064 /** 065 * Constructor without parameters, needs a call to `setMapping` later on in 066 * order to generate any Resources. 067 */ 068 public FHIRPathResourceGeneratorR4() { 069 this.pathMapping = new HashMap<String, String>(); 070 this.ctx = FhirContext.forR4(); 071 this.engine = new FHIRPathEngine(new HapiWorkerContext(ctx, ctx.getValidationSupport())); 072 } 073 074 /** 075 * Constructor that allows to provide a mapping right away. 076 * 077 * @param mapping Map<String, String> a mapping of FHIRPath to value Strings 078 * that will be used to create a Resource. 079 */ 080 public FHIRPathResourceGeneratorR4(Map<String, String> mapping) { 081 this(); 082 this.setMapping(mapping); 083 } 084 085 /** 086 * Setter for the FHIRPath mapping Map instance. 087 * 088 * @param mapping Map<String, String> a mapping of FHIRPath to value Strings 089 * that will be used to create a Resource. 090 */ 091 public void setMapping(Map<String, String> mapping) { 092 this.pathMapping = mapping; 093 } 094 095 /** 096 * Getter for a generated Resource. null if no Resource has been generated yet. 097 * 098 * @return T the generated Resource or null. 099 */ 100 public T getResource() { 101 return this.resource; 102 } 103 104 /** 105 * Prepares the internal state prior to generating a FHIR Resource. Called once 106 * upon generation at the start. 107 * 108 * @param resourceClass Class<T> The class of the Resource that shall be created 109 * (an empty Resource will be created in this method). 110 */ 111 @SuppressWarnings("unchecked") 112 private void prepareInternalState(Class<T> resourceClass) { 113 this.resource = (T) this.ctx.getResourceDefinition(resourceClass).newInstance(); 114 } 115 116 /** 117 * The generation method that yields a new instance of class `resourceClass` 118 * with every value set in the FHIRPath mapping. 119 * 120 * @param resourceClass Class<T> The class of the Resource that shall be 121 * created. 122 * @return T a new FHIR Resource instance of class `resourceClass`. 123 */ 124 public T generateResource(Class<T> resourceClass) { 125 this.prepareInternalState(resourceClass); 126 127 for (String fhirPath : this.sortedPaths()) { 128 // prepare the next fhirPath iteration: create a new nodeStack and set the value 129 this.nodeStack = new Stack<>(); 130 this.nodeStack.push(new GenerationTier(this.ctx.getResourceDefinition(this.resource), this.resource)); 131 this.valueToSet = this.pathMapping.get(fhirPath); 132 133 // pathNode is the part of the FHIRPath we are processing 134 ExpressionNode pathNode = this.engine.parse(fhirPath); 135 while (pathNode != null) { 136 switch (pathNode.getKind()) { 137 case Name: 138 this.handleNameNode(pathNode); 139 break; 140 case Function: 141 this.handleFunctionNode(pathNode); 142 break; 143 case Constant: 144 case Group: 145 case Unary: 146 // TODO: unimplmemented, what to do? 147 break; 148 } 149 pathNode = pathNode.getInner(); 150 } 151 } 152 153 this.nodeStack = null; 154 return this.resource; 155 } 156 157 /* 158 * Handling Named nodes 159 */ 160 161 /** 162 * Handles a named node, either adding a new layer to the `nodeStack` when 163 * reaching a Composite Node or adding the value for Primitive Nodes. 164 * 165 * @param fhirPath String the FHIRPath section for the next GenerationTier. 166 */ 167 private void handleNameNode(ExpressionNode fhirPath) { 168 BaseRuntimeChildDefinition childDef = 169 this.nodeStack.peek().nodeDefinition.getChildByName(fhirPath.getName()); 170 if (childDef == null) { 171 // nothing to do 172 return; 173 } 174 175 // identify the type of named node we need to handle here by getting the runtime 176 // definition type 177 switch (childDef.getChildByName(fhirPath.getName()).getChildType()) { 178 case COMPOSITE_DATATYPE: 179 handleCompositeNode(fhirPath); 180 break; 181 182 case PRIMITIVE_DATATYPE: 183 handlePrimitiveNode(fhirPath); 184 break; 185 186 case ID_DATATYPE: 187 case RESOURCE: 188 case CONTAINED_RESOURCE_LIST: 189 case CONTAINED_RESOURCES: 190 case EXTENSION_DECLARED: 191 case PRIMITIVE_XHTML: 192 case PRIMITIVE_XHTML_HL7ORG: 193 case RESOURCE_BLOCK: 194 case UNDECL_EXT: 195 // TODO: not implemented. What to do? 196 } 197 } 198 199 /** 200 * Handles primitive nodes with regards to the current latest tier of the 201 * nodeStack. Sets a primitive value to all nodes. 202 * 203 * @param fhirPath ExpressionNode segment of the fhirPath that specifies the 204 * primitive value to set. 205 */ 206 private void handlePrimitiveNode(ExpressionNode fhirPath) { 207 // Get the child definition from the parent 208 BaseRuntimeChildDefinition childDefinition = 209 this.nodeStack.peek().nodeDefinition.getChildByName(fhirPath.getName()); 210 // Get the primitive type definition from the childDeftinion 211 RuntimePrimitiveDatatypeDefinition primitiveTarget = 212 (RuntimePrimitiveDatatypeDefinition) childDefinition.getChildByName(fhirPath.getName()); 213 for (IBase nodeElement : this.nodeStack.peek().nodes) { 214 // add the primitive value to each parent node 215 IPrimitiveType<?> primitive = 216 primitiveTarget.newInstance(childDefinition.getInstanceConstructorArguments()); 217 primitive.setValueAsString(this.valueToSet); 218 childDefinition.getMutator().addValue(nodeElement, primitive); 219 } 220 } 221 222 /** 223 * Handles a composite node with regards to the current latest tier of the 224 * nodeStack. Creates a new node based on fhirPath if none are available. 225 * 226 * @param fhirPath ExpressionNode the segment of the FHIRPath that is being 227 * handled right now. 228 */ 229 private void handleCompositeNode(ExpressionNode fhirPath) { 230 GenerationTier nextTier = new GenerationTier(); 231 // get the name of the FHIRPath for the next tier 232 nextTier.fhirPathName = fhirPath.getName(); 233 // get the child definition from the parent nodePefinition 234 nextTier.childDefinition = this.nodeStack.peek().nodeDefinition.getChildByName(fhirPath.getName()); 235 // create a nodeDefinition for the next tier 236 nextTier.nodeDefinition = nextTier.childDefinition.getChildByName(nextTier.fhirPathName); 237 238 RuntimeCompositeDatatypeDefinition compositeTarget = 239 (RuntimeCompositeDatatypeDefinition) nextTier.nodeDefinition; 240 // iterate through all parent nodes 241 for (IBase nodeElement : this.nodeStack.peek().nodes) { 242 List<IBase> containedNodes = nextTier.childDefinition.getAccessor().getValues(nodeElement); 243 if (containedNodes.size() > 0) { 244 // check if sister nodes are already available 245 nextTier.nodes.addAll(containedNodes); 246 } else { 247 // if not nodes are available, create a new node 248 ICompositeType compositeNode = 249 compositeTarget.newInstance(nextTier.childDefinition.getInstanceConstructorArguments()); 250 nextTier.childDefinition.getMutator().addValue(nodeElement, compositeNode); 251 nextTier.nodes.add(compositeNode); 252 } 253 } 254 // push the created nextTier to the nodeStack 255 this.nodeStack.push(nextTier); 256 } 257 258 /* 259 * Handling Function Nodes 260 */ 261 262 /** 263 * Handles a function node of a FHIRPath. 264 * 265 * @param fhirPath ExpressionNode the segment of the FHIRPath that is being 266 * handled right now. 267 */ 268 private void handleFunctionNode(ExpressionNode fhirPath) { 269 switch (fhirPath.getFunction()) { 270 case Where: 271 this.handleWhereFunctionNode(fhirPath); 272 break; 273 case MatchesFull: 274 case Aggregate: 275 case All: 276 case AllFalse: 277 case AllTrue: 278 case AnyFalse: 279 case AnyTrue: 280 case As: 281 case Check: 282 case Children: 283 case Combine: 284 case ConformsTo: 285 case Contains: 286 case ConvertsToBoolean: 287 case ConvertsToDateTime: 288 case ConvertsToDecimal: 289 case ConvertsToInteger: 290 case ConvertsToQuantity: 291 case ConvertsToString: 292 case ConvertsToTime: 293 case Count: 294 case Custom: 295 case DefineVariable: 296 case Descendants: 297 case Distinct: 298 case Empty: 299 case EndsWith: 300 case Exclude: 301 case Exists: 302 case Extension: 303 case First: 304 case HasValue: 305 case Iif: 306 case IndexOf: 307 case Intersect: 308 case Is: 309 case IsDistinct: 310 case Item: 311 case Last: 312 case Length: 313 case Lower: 314 case Matches: 315 case MemberOf: 316 case Not: 317 case Now: 318 case OfType: 319 case Repeat: 320 case Replace: 321 case ReplaceMatches: 322 case Resolve: 323 case Select: 324 case Single: 325 case Skip: 326 case StartsWith: 327 case SubsetOf: 328 case Substring: 329 case SupersetOf: 330 case Tail: 331 case Take: 332 case ToBoolean: 333 case ToChars: 334 case ToDateTime: 335 case ToDecimal: 336 case ToInteger: 337 case ToQuantity: 338 case ToString: 339 case ToTime: 340 case Today: 341 case Trace: 342 case Type: 343 case Union: 344 case Upper: 345 // TODO: unimplemented, what to do? 346 case ConvertsToDate: 347 break; 348 case Round: 349 break; 350 case Sqrt: 351 break; 352 case Abs: 353 break; 354 case Ceiling: 355 break; 356 case Exp: 357 break; 358 case Floor: 359 break; 360 case Ln: 361 break; 362 case Log: 363 break; 364 case Power: 365 break; 366 case Truncate: 367 break; 368 case Encode: 369 break; 370 case Decode: 371 break; 372 case Escape: 373 break; 374 case Unescape: 375 break; 376 case Trim: 377 break; 378 case Split: 379 break; 380 case Join: 381 break; 382 case LowBoundary: 383 break; 384 case HighBoundary: 385 break; 386 case Precision: 387 break; 388 case HtmlChecks1: 389 break; 390 case HtmlChecks2: 391 break; 392 } 393 } 394 395 /** 396 * Handles a function node of a `where`-function. Iterates through all params 397 * and handle where functions for primitive datatypes (others are not 398 * implemented and yield errors.) 399 * 400 * @param fhirPath ExpressionNode the segment of the FHIRPath that contains the 401 * where function 402 */ 403 private void handleWhereFunctionNode(ExpressionNode fhirPath) { 404 // iterate through all where parameters 405 for (ExpressionNode param : fhirPath.getParameters()) { 406 BaseRuntimeChildDefinition wherePropertyChild = 407 this.nodeStack.peek().nodeDefinition.getChildByName(param.getName()); 408 BaseRuntimeElementDefinition<?> wherePropertyDefinition = 409 wherePropertyChild.getChildByName(param.getName()); 410 411 // only primitive nodes can be checked using the where function 412 switch (wherePropertyDefinition.getChildType()) { 413 case PRIMITIVE_DATATYPE: 414 this.handleWhereFunctionParam(param); 415 break; 416 case COMPOSITE_DATATYPE: 417 case CONTAINED_RESOURCES: 418 case CONTAINED_RESOURCE_LIST: 419 case EXTENSION_DECLARED: 420 case ID_DATATYPE: 421 case PRIMITIVE_XHTML: 422 case PRIMITIVE_XHTML_HL7ORG: 423 case RESOURCE: 424 case RESOURCE_BLOCK: 425 case UNDECL_EXT: 426 // TODO: unimplemented. What to do? 427 } 428 } 429 } 430 431 /** 432 * Filter the latest nodeStack tier using `param`. 433 * 434 * @param param ExpressionNode parameter type ExpressionNode that provides the 435 * where clause that is used to filter nodes from the nodeStack. 436 */ 437 private void handleWhereFunctionParam(ExpressionNode param) { 438 BaseRuntimeChildDefinition wherePropertyChild = 439 this.nodeStack.peek().nodeDefinition.getChildByName(param.getName()); 440 BaseRuntimeElementDefinition<?> wherePropertyDefinition = wherePropertyChild.getChildByName(param.getName()); 441 442 String matchingValue = param.getOpNext().getConstant().toString(); 443 List<IBase> matchingNodes = new ArrayList<>(); 444 List<IBase> unlabeledNodes = new ArrayList<>(); 445 // sort all nodes from the nodeStack into matching nodes and unlabeled nodes 446 for (IBase node : this.nodeStack.peek().nodes) { 447 List<IBase> operationValues = wherePropertyChild.getAccessor().getValues(node); 448 if (operationValues.size() == 0) { 449 unlabeledNodes.add(node); 450 } else { 451 for (IBase operationValue : operationValues) { 452 IPrimitiveType<?> primitive = (IPrimitiveType<?>) operationValue; 453 switch (param.getOperation()) { 454 case Equals: 455 if (primitive.getValueAsString().equals(matchingValue)) { 456 matchingNodes.add(node); 457 } 458 break; 459 case NotEquals: 460 if (!primitive.getValueAsString().equals(matchingValue)) { 461 matchingNodes.add(node); 462 } 463 break; 464 case And: 465 case As: 466 case Concatenate: 467 case Contains: 468 case Div: 469 case DivideBy: 470 case Equivalent: 471 case Greater: 472 case GreaterOrEqual: 473 case Implies: 474 case In: 475 case Is: 476 case LessOrEqual: 477 case LessThan: 478 case MemberOf: 479 case Minus: 480 case Mod: 481 case NotEquivalent: 482 case Or: 483 case Plus: 484 case Times: 485 case Union: 486 case Xor: 487 // TODO: unimplemented, what to do? 488 } 489 } 490 } 491 } 492 493 if (matchingNodes.size() == 0) { 494 if (unlabeledNodes.size() == 0) { 495 // no nodes were matched and no unlabeled nodes are available. We need to add a 496 // sister node to the nodeStack 497 GenerationTier latestTier = this.nodeStack.pop(); 498 GenerationTier previousTier = this.nodeStack.peek(); 499 this.nodeStack.push(latestTier); 500 501 RuntimeCompositeDatatypeDefinition compositeTarget = 502 (RuntimeCompositeDatatypeDefinition) latestTier.nodeDefinition; 503 ICompositeType compositeNode = 504 compositeTarget.newInstance(latestTier.childDefinition.getInstanceConstructorArguments()); 505 latestTier.childDefinition.getMutator().addValue(previousTier.nodes.get(0), compositeNode); 506 unlabeledNodes.add(compositeNode); 507 } 508 509 switch (param.getOperation()) { 510 case Equals: 511 // if we are checking for equality, we need to set the property we looked for on 512 // the unlabeled node(s) 513 RuntimePrimitiveDatatypeDefinition equalsPrimitive = 514 (RuntimePrimitiveDatatypeDefinition) wherePropertyDefinition; 515 IPrimitiveType<?> primitive = 516 equalsPrimitive.newInstance(wherePropertyChild.getInstanceConstructorArguments()); 517 primitive.setValueAsString(param.getOpNext().getConstant().toString()); 518 for (IBase node : unlabeledNodes) { 519 wherePropertyChild.getMutator().addValue(node, primitive); 520 matchingNodes.add(node); 521 } 522 break; 523 case NotEquals: 524 // if we are checking for inequality, we need to pass all unlabeled (or created 525 // if none were available) 526 matchingNodes.addAll(unlabeledNodes); 527 break; 528 case And: 529 case As: 530 case Concatenate: 531 case Contains: 532 case Div: 533 case DivideBy: 534 case Equivalent: 535 case Greater: 536 case GreaterOrEqual: 537 case Implies: 538 case In: 539 case Is: 540 case LessOrEqual: 541 case LessThan: 542 case MemberOf: 543 case Minus: 544 case Mod: 545 case NotEquivalent: 546 case Or: 547 case Plus: 548 case Times: 549 case Union: 550 case Xor: 551 // TODO: need to implement above first 552 } 553 } 554 555 // set the nodes to the filtered ones 556 this.nodeStack.peek().nodes = matchingNodes; 557 } 558 559 /** 560 * Creates a list all FHIRPaths from the mapping ordered by paths with where 561 * equals, where unequals and the rest. 562 * 563 * @return List<String> a List of FHIRPaths ordered by the type. 564 */ 565 private List<String> sortedPaths() { 566 List<String> whereEquals = new ArrayList<String>(); 567 List<String> whereUnequals = new ArrayList<String>(); 568 List<String> withoutWhere = new ArrayList<String>(); 569 570 for (String fhirPath : this.pathMapping.keySet()) { 571 switch (this.getTypeOfFhirPath(fhirPath)) { 572 case WHERE_EQUALS: 573 whereEquals.add(fhirPath); 574 break; 575 case WHERE_UNEQUALS: 576 whereUnequals.add(fhirPath); 577 break; 578 case WITHOUT_WHERE: 579 withoutWhere.add(fhirPath); 580 break; 581 } 582 } 583 584 List<String> ret = new ArrayList<String>(); 585 ret.addAll(whereEquals); 586 ret.addAll(whereUnequals); 587 ret.addAll(withoutWhere); 588 return ret; 589 } 590 591 /** 592 * Returns the type of path based on the FHIRPath String. 593 * 594 * @param fhirPath String representation of a FHIRPath. 595 * @return PathType the type of path supplied as `fhirPath`. 596 */ 597 private PathType getTypeOfFhirPath(String fhirPath) { 598 ExpressionNode fhirPathExpression = this.engine.parse(fhirPath); 599 while (fhirPathExpression != null) { 600 if (fhirPathExpression.getKind() == ExpressionNode.Kind.Function) { 601 if (fhirPathExpression.getFunction() == ExpressionNode.Function.Where) { 602 for (ExpressionNode params : fhirPathExpression.getParameters()) { 603 switch (params.getOperation()) { 604 case Equals: 605 return PathType.WHERE_EQUALS; 606 case NotEquals: 607 return PathType.WHERE_UNEQUALS; 608 case And: 609 case As: 610 case Concatenate: 611 case Contains: 612 case Div: 613 case DivideBy: 614 case Equivalent: 615 case Greater: 616 case GreaterOrEqual: 617 case Implies: 618 case In: 619 case Is: 620 case LessOrEqual: 621 case LessThan: 622 case MemberOf: 623 case Minus: 624 case Mod: 625 case NotEquivalent: 626 case Or: 627 case Plus: 628 case Times: 629 case Union: 630 case Xor: 631 // TODO: need to implement above first 632 } 633 } 634 } 635 } 636 fhirPathExpression = fhirPathExpression.getInner(); 637 } 638 return PathType.WITHOUT_WHERE; 639 } 640 641 /** 642 * A simple enum to diffirentiate between types of FHIRPaths in the special use 643 * case of generating FHIR Resources. 644 */ 645 public enum PathType { 646 WHERE_EQUALS, 647 WHERE_UNEQUALS, 648 WITHOUT_WHERE 649 } 650}