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.hapi.ctx.HapiWorkerContext; 012import org.hl7.fhir.r4.model.ExpressionNode; 013import org.hl7.fhir.r4.model.Resource; 014import org.hl7.fhir.r4.utils.FHIRPathEngine; 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 <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 Alias: 276 case AliasAs: 277 case All: 278 case AllFalse: 279 case AllTrue: 280 case AnyFalse: 281 case AnyTrue: 282 case As: 283 case Check: 284 case Children: 285 case Combine: 286 case ConformsTo: 287 case Contains: 288 case ConvertsToBoolean: 289 case ConvertsToDateTime: 290 case ConvertsToDecimal: 291 case ConvertsToInteger: 292 case ConvertsToQuantity: 293 case ConvertsToString: 294 case ConvertsToTime: 295 case Count: 296 case Custom: 297 case Descendants: 298 case Distinct: 299 case Empty: 300 case EndsWith: 301 case Exclude: 302 case Exists: 303 case Extension: 304 case First: 305 case HasValue: 306 case Iif: 307 case IndexOf: 308 case Intersect: 309 case Is: 310 case IsDistinct: 311 case Item: 312 case Last: 313 case Length: 314 case Lower: 315 case Matches: 316 case MemberOf: 317 case Not: 318 case Now: 319 case OfType: 320 case Repeat: 321 case Replace: 322 case ReplaceMatches: 323 case Resolve: 324 case Select: 325 case Single: 326 case Skip: 327 case StartsWith: 328 case SubsetOf: 329 case Substring: 330 case SupersetOf: 331 case Tail: 332 case Take: 333 case ToBoolean: 334 case ToChars: 335 case ToDateTime: 336 case ToDecimal: 337 case ToInteger: 338 case ToQuantity: 339 case ToString: 340 case ToTime: 341 case Today: 342 case Trace: 343 case Type: 344 case Union: 345 case Upper: 346 // TODO: unimplemented, what to do? 347 case ConvertsToDate: 348 break; 349 case Round: 350 break; 351 case Sqrt: 352 break; 353 case Abs: 354 break; 355 case Ceiling: 356 break; 357 case Exp: 358 break; 359 case Floor: 360 break; 361 case Ln: 362 break; 363 case Log: 364 break; 365 case Power: 366 break; 367 case Truncate: 368 break; 369 case Encode: 370 break; 371 case Decode: 372 break; 373 case Escape: 374 break; 375 case Unescape: 376 break; 377 case Trim: 378 break; 379 case Split: 380 break; 381 case Join: 382 break; 383 case LowBoundary: 384 break; 385 case HighBoundary: 386 break; 387 case Precision: 388 break; 389 case HtmlChecks1: 390 break; 391 case HtmlChecks2: 392 break; 393 } 394 } 395 396 /** 397 * Handles a function node of a `where`-function. Iterates through all params 398 * and handle where functions for primitive datatypes (others are not 399 * implemented and yield errors.) 400 * 401 * @param fhirPath ExpressionNode the segment of the FHIRPath that contains the 402 * where function 403 */ 404 private void handleWhereFunctionNode(ExpressionNode fhirPath) { 405 // iterate through all where parameters 406 for (ExpressionNode param : fhirPath.getParameters()) { 407 BaseRuntimeChildDefinition wherePropertyChild = 408 this.nodeStack.peek().nodeDefinition.getChildByName(param.getName()); 409 BaseRuntimeElementDefinition<?> wherePropertyDefinition = 410 wherePropertyChild.getChildByName(param.getName()); 411 412 // only primitive nodes can be checked using the where function 413 switch (wherePropertyDefinition.getChildType()) { 414 case PRIMITIVE_DATATYPE: 415 this.handleWhereFunctionParam(param); 416 break; 417 case COMPOSITE_DATATYPE: 418 case CONTAINED_RESOURCES: 419 case CONTAINED_RESOURCE_LIST: 420 case EXTENSION_DECLARED: 421 case ID_DATATYPE: 422 case PRIMITIVE_XHTML: 423 case PRIMITIVE_XHTML_HL7ORG: 424 case RESOURCE: 425 case RESOURCE_BLOCK: 426 case UNDECL_EXT: 427 // TODO: unimplemented. What to do? 428 } 429 } 430 } 431 432 /** 433 * Filter the latest nodeStack tier using `param`. 434 * 435 * @param param ExpressionNode parameter type ExpressionNode that provides the 436 * where clause that is used to filter nodes from the nodeStack. 437 */ 438 private void handleWhereFunctionParam(ExpressionNode param) { 439 BaseRuntimeChildDefinition wherePropertyChild = 440 this.nodeStack.peek().nodeDefinition.getChildByName(param.getName()); 441 BaseRuntimeElementDefinition<?> wherePropertyDefinition = wherePropertyChild.getChildByName(param.getName()); 442 443 String matchingValue = param.getOpNext().getConstant().toString(); 444 List<IBase> matchingNodes = new ArrayList<>(); 445 List<IBase> unlabeledNodes = new ArrayList<>(); 446 // sort all nodes from the nodeStack into matching nodes and unlabeled nodes 447 for (IBase node : this.nodeStack.peek().nodes) { 448 List<IBase> operationValues = wherePropertyChild.getAccessor().getValues(node); 449 if (operationValues.size() == 0) { 450 unlabeledNodes.add(node); 451 } else { 452 for (IBase operationValue : operationValues) { 453 IPrimitiveType<?> primitive = (IPrimitiveType<?>) operationValue; 454 switch (param.getOperation()) { 455 case Equals: 456 if (primitive.getValueAsString().equals(matchingValue)) { 457 matchingNodes.add(node); 458 } 459 break; 460 case NotEquals: 461 if (!primitive.getValueAsString().equals(matchingValue)) { 462 matchingNodes.add(node); 463 } 464 break; 465 case And: 466 case As: 467 case Concatenate: 468 case Contains: 469 case Div: 470 case DivideBy: 471 case Equivalent: 472 case Greater: 473 case GreaterOrEqual: 474 case Implies: 475 case In: 476 case Is: 477 case LessOrEqual: 478 case LessThan: 479 case MemberOf: 480 case Minus: 481 case Mod: 482 case NotEquivalent: 483 case Or: 484 case Plus: 485 case Times: 486 case Union: 487 case Xor: 488 // TODO: unimplemented, what to do? 489 } 490 } 491 } 492 } 493 494 if (matchingNodes.size() == 0) { 495 if (unlabeledNodes.size() == 0) { 496 // no nodes were matched and no unlabeled nodes are available. We need to add a 497 // sister node to the nodeStack 498 GenerationTier latestTier = this.nodeStack.pop(); 499 GenerationTier previousTier = this.nodeStack.peek(); 500 this.nodeStack.push(latestTier); 501 502 RuntimeCompositeDatatypeDefinition compositeTarget = 503 (RuntimeCompositeDatatypeDefinition) latestTier.nodeDefinition; 504 ICompositeType compositeNode = 505 compositeTarget.newInstance(latestTier.childDefinition.getInstanceConstructorArguments()); 506 latestTier.childDefinition.getMutator().addValue(previousTier.nodes.get(0), compositeNode); 507 unlabeledNodes.add(compositeNode); 508 } 509 510 switch (param.getOperation()) { 511 case Equals: 512 // if we are checking for equality, we need to set the property we looked for on 513 // the unlabeled node(s) 514 RuntimePrimitiveDatatypeDefinition equalsPrimitive = 515 (RuntimePrimitiveDatatypeDefinition) wherePropertyDefinition; 516 IPrimitiveType<?> primitive = 517 equalsPrimitive.newInstance(wherePropertyChild.getInstanceConstructorArguments()); 518 primitive.setValueAsString(param.getOpNext().getConstant().toString()); 519 for (IBase node : unlabeledNodes) { 520 wherePropertyChild.getMutator().addValue(node, primitive); 521 matchingNodes.add(node); 522 } 523 break; 524 case NotEquals: 525 // if we are checking for inequality, we need to pass all unlabeled (or created 526 // if none were available) 527 matchingNodes.addAll(unlabeledNodes); 528 break; 529 case And: 530 case As: 531 case Concatenate: 532 case Contains: 533 case Div: 534 case DivideBy: 535 case Equivalent: 536 case Greater: 537 case GreaterOrEqual: 538 case Implies: 539 case In: 540 case Is: 541 case LessOrEqual: 542 case LessThan: 543 case MemberOf: 544 case Minus: 545 case Mod: 546 case NotEquivalent: 547 case Or: 548 case Plus: 549 case Times: 550 case Union: 551 case Xor: 552 // TODO: need to implement above first 553 } 554 } 555 556 // set the nodes to the filtered ones 557 this.nodeStack.peek().nodes = matchingNodes; 558 } 559 560 /** 561 * Creates a list all FHIRPaths from the mapping ordered by paths with where 562 * equals, where unequals and the rest. 563 * 564 * @return List<String> a List of FHIRPaths ordered by the type. 565 */ 566 private List<String> sortedPaths() { 567 List<String> whereEquals = new ArrayList<String>(); 568 List<String> whereUnequals = new ArrayList<String>(); 569 List<String> withoutWhere = new ArrayList<String>(); 570 571 for (String fhirPath : this.pathMapping.keySet()) { 572 switch (this.getTypeOfFhirPath(fhirPath)) { 573 case WHERE_EQUALS: 574 whereEquals.add(fhirPath); 575 break; 576 case WHERE_UNEQUALS: 577 whereUnequals.add(fhirPath); 578 break; 579 case WITHOUT_WHERE: 580 withoutWhere.add(fhirPath); 581 break; 582 } 583 } 584 585 List<String> ret = new ArrayList<String>(); 586 ret.addAll(whereEquals); 587 ret.addAll(whereUnequals); 588 ret.addAll(withoutWhere); 589 return ret; 590 } 591 592 /** 593 * Returns the type of path based on the FHIRPath String. 594 * 595 * @param fhirPath String representation of a FHIRPath. 596 * @return PathType the type of path supplied as `fhirPath`. 597 */ 598 private PathType getTypeOfFhirPath(String fhirPath) { 599 ExpressionNode fhirPathExpression = this.engine.parse(fhirPath); 600 while (fhirPathExpression != null) { 601 if (fhirPathExpression.getKind() == ExpressionNode.Kind.Function) { 602 if (fhirPathExpression.getFunction() == ExpressionNode.Function.Where) { 603 for (ExpressionNode params : fhirPathExpression.getParameters()) { 604 switch (params.getOperation()) { 605 case Equals: 606 return PathType.WHERE_EQUALS; 607 case NotEquals: 608 return PathType.WHERE_UNEQUALS; 609 case And: 610 case As: 611 case Concatenate: 612 case Contains: 613 case Div: 614 case DivideBy: 615 case Equivalent: 616 case Greater: 617 case GreaterOrEqual: 618 case Implies: 619 case In: 620 case Is: 621 case LessOrEqual: 622 case LessThan: 623 case MemberOf: 624 case Minus: 625 case Mod: 626 case NotEquivalent: 627 case Or: 628 case Plus: 629 case Times: 630 case Union: 631 case Xor: 632 // TODO: need to implement above first 633 } 634 } 635 } 636 } 637 fhirPathExpression = fhirPathExpression.getInner(); 638 } 639 return PathType.WITHOUT_WHERE; 640 } 641 642 /** 643 * A simple enum to diffirentiate between types of FHIRPaths in the special use 644 * case of generating FHIR Resources. 645 */ 646 public enum PathType { 647 WHERE_EQUALS, 648 WHERE_UNEQUALS, 649 WITHOUT_WHERE 650 } 651}