001package org.hl7.fhir.common.hapi.validation.validator; 002 003import java.util.ArrayList; 004import java.util.HashMap; 005import java.util.List; 006import java.util.Map; 007import java.util.Stack; 008 009import org.hl7.fhir.r4.utils.FHIRPathEngine; 010import org.hl7.fhir.instance.model.api.IBase; 011import org.hl7.fhir.instance.model.api.ICompositeType; 012import org.hl7.fhir.instance.model.api.IPrimitiveType; 013import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext; 014import org.hl7.fhir.r4.model.ExpressionNode; 015import org.hl7.fhir.r4.model.Resource; 016 017import ca.uhn.fhir.context.BaseRuntimeChildDefinition; 018import ca.uhn.fhir.context.BaseRuntimeElementDefinition; 019import ca.uhn.fhir.context.FhirContext; 020import ca.uhn.fhir.context.RuntimeCompositeDatatypeDefinition; 021import ca.uhn.fhir.context.RuntimePrimitiveDatatypeDefinition; 022 023/** 024 * This class can be used to generate resources using FHIRPath expressions. 025 * 026 * Note that this is an experimental feature and the API is expected to change. Ideally 027 * this will be made version independent and moved out of the validation module 028 * in a future release. 029 * 030 * @author Marcel Parciak <marcel.parciak@med.uni-goettingen.de> 031 */ 032public class FHIRPathResourceGeneratorR4<T extends Resource> { 033 034 private FhirContext ctx; 035 private FHIRPathEngine engine; 036 private Map<String, String> pathMapping; 037 private T resource = null; 038 039 private String valueToSet = null; 040 private Stack<GenerationTier> nodeStack = null; 041 042 /** 043 * The GenerationTier summarizes some variables that are needed to create FHIR 044 * elements later on. 045 */ 046 class GenerationTier { 047 // The RuntimeDefinition of nodes 048 public BaseRuntimeElementDefinition<?> nodeDefinition = null; 049 // The actual nodes, i.e. the instances that hold the values 050 public List<IBase> nodes = new ArrayList<>(); 051 // The ChildDefinition applied to the parent (i.e. one of the nodes from a lower 052 // GenerationTier) to create nodes 053 public BaseRuntimeChildDefinition childDefinition = null; 054 // The path segment name of nodes 055 public String fhirPathName = null; 056 057 public GenerationTier() { 058 } 059 060 public GenerationTier(BaseRuntimeElementDefinition<?> nodeDef, IBase firstNode) { 061 this.nodeDefinition = nodeDef; 062 this.nodes.add(firstNode); 063 } 064 } 065 066 /** 067 * Constructor without parameters, needs a call to `setMapping` later on in 068 * order to generate any Resources. 069 */ 070 public FHIRPathResourceGeneratorR4() { 071 this.pathMapping = new HashMap<String, String>(); 072 this.ctx = FhirContext.forR4(); 073 this.engine = new FHIRPathEngine(new HapiWorkerContext(ctx, ctx.getValidationSupport())); 074 } 075 076 /** 077 * Constructor that allows to provide a mapping right away. 078 * 079 * @param mapping Map<String, String> a mapping of FHIRPath to value Strings 080 * that will be used to create a Resource. 081 */ 082 public FHIRPathResourceGeneratorR4(Map<String, String> mapping) { 083 this(); 084 this.setMapping(mapping); 085 } 086 087 /** 088 * Setter for the FHIRPath mapping Map instance. 089 * 090 * @param mapping Map<String, String> a mapping of FHIRPath to value Strings 091 * that will be used to create a Resource. 092 */ 093 public void setMapping(Map<String, String> mapping) { 094 this.pathMapping = mapping; 095 } 096 097 /** 098 * Getter for a generated Resource. null if no Resource has been generated yet. 099 * 100 * @return T the generated Resource or null. 101 */ 102 public T getResource() { 103 return this.resource; 104 } 105 106 /** 107 * Prepares the internal state prior to generating a FHIR Resource. Called once 108 * upon generation at the start. 109 * 110 * @param resourceClass Class<T> The class of the Resource that shall be created 111 * (an empty Resource will be created in this method). 112 */ 113 @SuppressWarnings("unchecked") 114 private void prepareInternalState(Class<T> resourceClass) { 115 this.resource = (T) this.ctx.getResourceDefinition(resourceClass).newInstance(); 116 } 117 118 /** 119 * The generation method that yields a new instance of class `resourceClass` 120 * with every value set in the FHIRPath mapping. 121 * 122 * @param resourceClass Class<T> The class of the Resource that shall be 123 * created. 124 * @return T a new FHIR Resource instance of class `resourceClass`. 125 */ 126 public T generateResource(Class<T> resourceClass) { 127 this.prepareInternalState(resourceClass); 128 129 for (String fhirPath : this.sortedPaths()) { 130 // prepare the next fhirPath iteration: create a new nodeStack and set the value 131 this.nodeStack = new Stack<>(); 132 this.nodeStack.push(new GenerationTier(this.ctx.getResourceDefinition(this.resource), this.resource)); 133 this.valueToSet = this.pathMapping.get(fhirPath); 134 135 // pathNode is the part of the FHIRPath we are processing 136 ExpressionNode pathNode = this.engine.parse(fhirPath); 137 while (pathNode != null) { 138 switch (pathNode.getKind()) { 139 case Name: 140 this.handleNameNode(pathNode); 141 break; 142 case Function: 143 this.handleFunctionNode(pathNode); 144 break; 145 case Constant: 146 case Group: 147 case Unary: 148 // TODO: unimplmemented, what to do? 149 break; 150 } 151 pathNode = pathNode.getInner(); 152 } 153 } 154 155 this.nodeStack = null; 156 return this.resource; 157 } 158 159 /* 160 * Handling Named nodes 161 */ 162 163 /** 164 * Handles a named node, either adding a new layer to the `nodeStack` when 165 * reaching a Composite Node or adding the value for Primitive Nodes. 166 * 167 * @param fhirPath String the FHIRPath section for the next GenerationTier. 168 */ 169 private void handleNameNode(ExpressionNode fhirPath) { 170 BaseRuntimeChildDefinition childDef = this.nodeStack.peek().nodeDefinition.getChildByName(fhirPath.getName()); 171 if (childDef == null) { 172 // nothing to do 173 return; 174 } 175 176 // identify the type of named node we need to handle here by getting the runtime 177 // definition type 178 switch (childDef.getChildByName(fhirPath.getName()).getChildType()) { 179 case COMPOSITE_DATATYPE: 180 handleCompositeNode(fhirPath); 181 break; 182 183 case PRIMITIVE_DATATYPE: 184 handlePrimitiveNode(fhirPath); 185 break; 186 187 case ID_DATATYPE: 188 case RESOURCE: 189 case CONTAINED_RESOURCE_LIST: 190 case CONTAINED_RESOURCES: 191 case EXTENSION_DECLARED: 192 case PRIMITIVE_XHTML: 193 case PRIMITIVE_XHTML_HL7ORG: 194 case RESOURCE_BLOCK: 195 case UNDECL_EXT: 196 // TODO: not implemented. What to do? 197 } 198 } 199 200 /** 201 * Handles primitive nodes with regards to the current latest tier of the 202 * nodeStack. Sets a primitive value to all nodes. 203 * 204 * @param fhirPath ExpressionNode segment of the fhirPath that specifies the 205 * primitive value to set. 206 */ 207 private void handlePrimitiveNode(ExpressionNode fhirPath) { 208 // Get the child definition from the parent 209 BaseRuntimeChildDefinition childDefinition = this.nodeStack.peek().nodeDefinition 210 .getChildByName(fhirPath.getName()); 211 // Get the primitive type definition from the childDeftinion 212 RuntimePrimitiveDatatypeDefinition primitiveTarget = (RuntimePrimitiveDatatypeDefinition) childDefinition 213 .getChildByName(fhirPath.getName()); 214 for (IBase nodeElement : this.nodeStack.peek().nodes) { 215 // add the primitive value to each parent node 216 IPrimitiveType<?> primitive = primitiveTarget 217 .newInstance(childDefinition.getInstanceConstructorArguments()); 218 primitive.setValueAsString(this.valueToSet); 219 childDefinition.getMutator().addValue(nodeElement, primitive); 220 } 221 } 222 223 /** 224 * Handles a composite node with regards to the current latest tier of the 225 * nodeStack. Creates a new node based on fhirPath if none are available. 226 * 227 * @param fhirPath ExpressionNode the segment of the FHIRPath that is being 228 * handled right now. 229 */ 230 private void handleCompositeNode(ExpressionNode fhirPath) { 231 GenerationTier nextTier = new GenerationTier(); 232 // get the name of the FHIRPath for the next tier 233 nextTier.fhirPathName = fhirPath.getName(); 234 // get the child definition from the parent nodePefinition 235 nextTier.childDefinition = this.nodeStack.peek().nodeDefinition.getChildByName(fhirPath.getName()); 236 // create a nodeDefinition for the next tier 237 nextTier.nodeDefinition = nextTier.childDefinition.getChildByName(nextTier.fhirPathName); 238 239 RuntimeCompositeDatatypeDefinition compositeTarget = (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 = compositeTarget 249 .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 = this.nodeStack.peek().nodeDefinition 408 .getChildByName(param.getName()); 409 BaseRuntimeElementDefinition<?> wherePropertyDefinition = wherePropertyChild 410 .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 = this.nodeStack.peek().nodeDefinition 440 .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 = (RuntimeCompositeDatatypeDefinition) latestTier.nodeDefinition; 503 ICompositeType compositeNode = compositeTarget 504 .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 = (RuntimePrimitiveDatatypeDefinition) wherePropertyDefinition; 514 IPrimitiveType<?> primitive = equalsPrimitive 515 .newInstance(wherePropertyChild.getInstanceConstructorArguments()); 516 primitive.setValueAsString(param.getOpNext().getConstant().toString()); 517 for (IBase node : unlabeledNodes) { 518 wherePropertyChild.getMutator().addValue(node, primitive); 519 matchingNodes.add(node); 520 } 521 break; 522 case NotEquals: 523 // if we are checking for inequality, we need to pass all unlabeled (or created 524 // if none were available) 525 matchingNodes.addAll(unlabeledNodes); 526 break; 527 case And: 528 case As: 529 case Concatenate: 530 case Contains: 531 case Div: 532 case DivideBy: 533 case Equivalent: 534 case Greater: 535 case GreaterOrEqual: 536 case Implies: 537 case In: 538 case Is: 539 case LessOrEqual: 540 case LessThan: 541 case MemberOf: 542 case Minus: 543 case Mod: 544 case NotEquivalent: 545 case Or: 546 case Plus: 547 case Times: 548 case Union: 549 case Xor: 550 // TODO: need to implement above first 551 } 552 } 553 554 // set the nodes to the filtered ones 555 this.nodeStack.peek().nodes = matchingNodes; 556 } 557 558 /** 559 * Creates a list all FHIRPaths from the mapping ordered by paths with where 560 * equals, where unequals and the rest. 561 * 562 * @return List<String> a List of FHIRPaths ordered by the type. 563 */ 564 private List<String> sortedPaths() { 565 List<String> whereEquals = new ArrayList<String>(); 566 List<String> whereUnequals = new ArrayList<String>(); 567 List<String> withoutWhere = new ArrayList<String>(); 568 569 for (String fhirPath : this.pathMapping.keySet()) { 570 switch (this.getTypeOfFhirPath(fhirPath)) { 571 case WHERE_EQUALS: 572 whereEquals.add(fhirPath); 573 break; 574 case WHERE_UNEQUALS: 575 whereUnequals.add(fhirPath); 576 break; 577 case WITHOUT_WHERE: 578 withoutWhere.add(fhirPath); 579 break; 580 } 581 } 582 583 List<String> ret = new ArrayList<String>(); 584 ret.addAll(whereEquals); 585 ret.addAll(whereUnequals); 586 ret.addAll(withoutWhere); 587 return ret; 588 } 589 590 /** 591 * Returns the type of path based on the FHIRPath String. 592 * 593 * @param fhirPath String representation of a FHIRPath. 594 * @return PathType the type of path supplied as `fhirPath`. 595 */ 596 private PathType getTypeOfFhirPath(String fhirPath) { 597 ExpressionNode fhirPathExpression = this.engine.parse(fhirPath); 598 while (fhirPathExpression != null) { 599 if (fhirPathExpression.getKind() == ExpressionNode.Kind.Function) { 600 if (fhirPathExpression.getFunction() == ExpressionNode.Function.Where) { 601 for (ExpressionNode params : fhirPathExpression.getParameters()) { 602 switch (params.getOperation()) { 603 case Equals: 604 return PathType.WHERE_EQUALS; 605 case NotEquals: 606 return PathType.WHERE_UNEQUALS; 607 case And: 608 case As: 609 case Concatenate: 610 case Contains: 611 case Div: 612 case DivideBy: 613 case Equivalent: 614 case Greater: 615 case GreaterOrEqual: 616 case Implies: 617 case In: 618 case Is: 619 case LessOrEqual: 620 case LessThan: 621 case MemberOf: 622 case Minus: 623 case Mod: 624 case NotEquivalent: 625 case Or: 626 case Plus: 627 case Times: 628 case Union: 629 case Xor: 630 // TODO: need to implement above first 631 } 632 } 633 } 634 } 635 fhirPathExpression = fhirPathExpression.getInner(); 636 } 637 return PathType.WITHOUT_WHERE; 638 } 639 640 /** 641 * A simple enum to diffirentiate between types of FHIRPaths in the special use 642 * case of generating FHIR Resources. 643 */ 644 public enum PathType { 645 WHERE_EQUALS, WHERE_UNEQUALS, WITHOUT_WHERE 646 } 647}