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 * @param value String the value that shall be set upon reaching a 169 * PrimitiveNode. 170 */ 171 private void handleNameNode(ExpressionNode fhirPath) { 172 BaseRuntimeChildDefinition childDef = this.nodeStack.peek().nodeDefinition.getChildByName(fhirPath.getName()); 173 if (childDef == null) { 174 // nothing to do 175 return; 176 } 177 178 // identify the type of named node we need to handle here by getting the runtime 179 // definition type 180 switch (childDef.getChildByName(fhirPath.getName()).getChildType()) { 181 case COMPOSITE_DATATYPE: 182 handleCompositeNode(fhirPath); 183 break; 184 185 case PRIMITIVE_DATATYPE: 186 handlePrimitiveNode(fhirPath); 187 break; 188 189 case ID_DATATYPE: 190 case RESOURCE: 191 case CONTAINED_RESOURCE_LIST: 192 case CONTAINED_RESOURCES: 193 case EXTENSION_DECLARED: 194 case PRIMITIVE_XHTML: 195 case PRIMITIVE_XHTML_HL7ORG: 196 case RESOURCE_BLOCK: 197 case UNDECL_EXT: 198 // TODO: not implemented. What to do? 199 } 200 } 201 202 /** 203 * Handles primitive nodes with regards to the current latest tier of the 204 * nodeStack. Sets a primitive value to all nodes. 205 * 206 * @param fhirPath ExpressionNode segment of the fhirPath that specifies the 207 * primitive value to set. 208 */ 209 private void handlePrimitiveNode(ExpressionNode fhirPath) { 210 // Get the child definition from the parent 211 BaseRuntimeChildDefinition childDefinition = this.nodeStack.peek().nodeDefinition 212 .getChildByName(fhirPath.getName()); 213 // Get the primitive type definition from the childDeftinion 214 RuntimePrimitiveDatatypeDefinition primitiveTarget = (RuntimePrimitiveDatatypeDefinition) childDefinition 215 .getChildByName(fhirPath.getName()); 216 for (IBase nodeElement : this.nodeStack.peek().nodes) { 217 // add the primitive value to each parent node 218 IPrimitiveType<?> primitive = primitiveTarget 219 .newInstance(childDefinition.getInstanceConstructorArguments()); 220 primitive.setValueAsString(this.valueToSet); 221 childDefinition.getMutator().addValue(nodeElement, primitive); 222 } 223 } 224 225 /** 226 * Handles a composite node with regards to the current latest tier of the 227 * nodeStack. Creates a new node based on fhirPath if none are available. 228 * 229 * @param fhirPath ExpressionNode the segment of the FHIRPath that is being 230 * handled right now. 231 */ 232 private void handleCompositeNode(ExpressionNode fhirPath) { 233 GenerationTier nextTier = new GenerationTier(); 234 // get the name of the FHIRPath for the next tier 235 nextTier.fhirPathName = fhirPath.getName(); 236 // get the child definition from the parent nodePefinition 237 nextTier.childDefinition = this.nodeStack.peek().nodeDefinition.getChildByName(fhirPath.getName()); 238 // create a nodeDefinition for the next tier 239 nextTier.nodeDefinition = nextTier.childDefinition.getChildByName(nextTier.fhirPathName); 240 241 RuntimeCompositeDatatypeDefinition compositeTarget = (RuntimeCompositeDatatypeDefinition) nextTier.nodeDefinition; 242 // iterate through all parent nodes 243 for (IBase nodeElement : this.nodeStack.peek().nodes) { 244 List<IBase> containedNodes = nextTier.childDefinition.getAccessor().getValues(nodeElement); 245 if (containedNodes.size() > 0) { 246 // check if sister nodes are already available 247 nextTier.nodes.addAll(containedNodes); 248 } else { 249 // if not nodes are available, create a new node 250 ICompositeType compositeNode = compositeTarget 251 .newInstance(nextTier.childDefinition.getInstanceConstructorArguments()); 252 nextTier.childDefinition.getMutator().addValue(nodeElement, compositeNode); 253 nextTier.nodes.add(compositeNode); 254 } 255 } 256 // push the created nextTier to the nodeStack 257 this.nodeStack.push(nextTier); 258 } 259 260 /* 261 * Handling Function Nodes 262 */ 263 264 /** 265 * Handles a function node of a FHIRPath. 266 * 267 * @param fhirPath ExpressionNode the segment of the FHIRPath that is being 268 * handled right now. 269 */ 270 private void handleFunctionNode(ExpressionNode fhirPath) { 271 switch(fhirPath.getFunction()) { 272 case Where: 273 this.handleWhereFunctionNode(fhirPath); 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 HtmlChecks: 307 case Iif: 308 case IndexOf: 309 case Intersect: 310 case Is: 311 case IsDistinct: 312 case Item: 313 case Last: 314 case Length: 315 case Lower: 316 case Matches: 317 case MemberOf: 318 case Not: 319 case Now: 320 case OfType: 321 case Repeat: 322 case Replace: 323 case ReplaceMatches: 324 case Resolve: 325 case Select: 326 case Single: 327 case Skip: 328 case StartsWith: 329 case SubsetOf: 330 case Substring: 331 case SupersetOf: 332 case Tail: 333 case Take: 334 case ToBoolean: 335 case ToChars: 336 case ToDateTime: 337 case ToDecimal: 338 case ToInteger: 339 case ToQuantity: 340 case ToString: 341 case ToTime: 342 case Today: 343 case Trace: 344 case Type: 345 case Union: 346 case Upper: 347 // TODO: unimplemented, what to do? 348 } 349 } 350 351 /** 352 * Handles a function node of a `where`-function. Iterates through all params 353 * and handle where functions for primitive datatypes (others are not 354 * implemented and yield errors.) 355 * 356 * @param fhirPath ExpressionNode the segment of the FHIRPath that contains the 357 * where function 358 */ 359 private void handleWhereFunctionNode(ExpressionNode fhirPath) { 360 // iterate through all where parameters 361 for (ExpressionNode param : fhirPath.getParameters()) { 362 BaseRuntimeChildDefinition wherePropertyChild = this.nodeStack.peek().nodeDefinition 363 .getChildByName(param.getName()); 364 BaseRuntimeElementDefinition<?> wherePropertyDefinition = wherePropertyChild 365 .getChildByName(param.getName()); 366 367 // only primitive nodes can be checked using the where function 368 switch(wherePropertyDefinition.getChildType()) { 369 case PRIMITIVE_DATATYPE: 370 this.handleWhereFunctionParam(param); 371 break; 372 case COMPOSITE_DATATYPE: 373 case CONTAINED_RESOURCES: 374 case CONTAINED_RESOURCE_LIST: 375 case EXTENSION_DECLARED: 376 case ID_DATATYPE: 377 case PRIMITIVE_XHTML: 378 case PRIMITIVE_XHTML_HL7ORG: 379 case RESOURCE: 380 case RESOURCE_BLOCK: 381 case UNDECL_EXT: 382 // TODO: unimplemented. What to do? 383 } 384 } 385 } 386 387 /** 388 * Filter the latest nodeStack tier using `param`. 389 * 390 * @param param ExpressionNode parameter type ExpressionNode that provides the 391 * where clause that is used to filter nodes from the nodeStack. 392 */ 393 private void handleWhereFunctionParam(ExpressionNode param) { 394 BaseRuntimeChildDefinition wherePropertyChild = this.nodeStack.peek().nodeDefinition 395 .getChildByName(param.getName()); 396 BaseRuntimeElementDefinition<?> wherePropertyDefinition = wherePropertyChild.getChildByName(param.getName()); 397 398 String matchingValue = param.getOpNext().getConstant().toString(); 399 List<IBase> matchingNodes = new ArrayList<>(); 400 List<IBase> unlabeledNodes = new ArrayList<>(); 401 // sort all nodes from the nodeStack into matching nodes and unlabeled nodes 402 for (IBase node : this.nodeStack.peek().nodes) { 403 List<IBase> operationValues = wherePropertyChild.getAccessor().getValues(node); 404 if (operationValues.size() == 0) { 405 unlabeledNodes.add(node); 406 } else { 407 for (IBase operationValue : operationValues) { 408 IPrimitiveType<?> primitive = (IPrimitiveType<?>) operationValue; 409 switch (param.getOperation()) { 410 case Equals: 411 if (primitive.getValueAsString().equals(matchingValue)) { 412 matchingNodes.add(node); 413 } 414 break; 415 case NotEquals: 416 if (!primitive.getValueAsString().equals(matchingValue)) { 417 matchingNodes.add(node); 418 } 419 break; 420 case And: 421 case As: 422 case Concatenate: 423 case Contains: 424 case Div: 425 case DivideBy: 426 case Equivalent: 427 case Greater: 428 case GreaterOrEqual: 429 case Implies: 430 case In: 431 case Is: 432 case LessOrEqual: 433 case LessThan: 434 case MemberOf: 435 case Minus: 436 case Mod: 437 case NotEquivalent: 438 case Or: 439 case Plus: 440 case Times: 441 case Union: 442 case Xor: 443 // TODO: unimplemented, what to do? 444 } 445 } 446 } 447 } 448 449 if (matchingNodes.size() == 0) { 450 if (unlabeledNodes.size() == 0) { 451 // no nodes were matched and no unlabeled nodes are available. We need to add a 452 // sister node to the nodeStack 453 GenerationTier latestTier = this.nodeStack.pop(); 454 GenerationTier previousTier = this.nodeStack.peek(); 455 this.nodeStack.push(latestTier); 456 457 RuntimeCompositeDatatypeDefinition compositeTarget = (RuntimeCompositeDatatypeDefinition) latestTier.nodeDefinition; 458 ICompositeType compositeNode = compositeTarget 459 .newInstance(latestTier.childDefinition.getInstanceConstructorArguments()); 460 latestTier.childDefinition.getMutator().addValue(previousTier.nodes.get(0), compositeNode); 461 unlabeledNodes.add(compositeNode); 462 } 463 464 switch(param.getOperation()) { 465 case Equals: 466 // if we are checking for equality, we need to set the property we looked for on 467 // the unlabeled node(s) 468 RuntimePrimitiveDatatypeDefinition equalsPrimitive = (RuntimePrimitiveDatatypeDefinition) wherePropertyDefinition; 469 IPrimitiveType<?> primitive = equalsPrimitive 470 .newInstance(wherePropertyChild.getInstanceConstructorArguments()); 471 primitive.setValueAsString(param.getOpNext().getConstant().toString()); 472 for (IBase node : unlabeledNodes) { 473 wherePropertyChild.getMutator().addValue(node, primitive); 474 matchingNodes.add(node); 475 } 476 break; 477 case NotEquals: 478 // if we are checking for inequality, we need to pass all unlabeled (or created 479 // if none were available) 480 matchingNodes.addAll(unlabeledNodes); 481 break; 482 case And: 483 case As: 484 case Concatenate: 485 case Contains: 486 case Div: 487 case DivideBy: 488 case Equivalent: 489 case Greater: 490 case GreaterOrEqual: 491 case Implies: 492 case In: 493 case Is: 494 case LessOrEqual: 495 case LessThan: 496 case MemberOf: 497 case Minus: 498 case Mod: 499 case NotEquivalent: 500 case Or: 501 case Plus: 502 case Times: 503 case Union: 504 case Xor: 505 // TODO: need to implement above first 506 } 507 } 508 509 // set the nodes to the filtered ones 510 this.nodeStack.peek().nodes = matchingNodes; 511 } 512 513 /** 514 * Creates a list all FHIRPaths from the mapping ordered by paths with where 515 * equals, where unequals and the rest. 516 * 517 * @return List<String> a List of FHIRPaths ordered by the type. 518 */ 519 private List<String> sortedPaths() { 520 List<String> whereEquals = new ArrayList<String>(); 521 List<String> whereUnequals = new ArrayList<String>(); 522 List<String> withoutWhere = new ArrayList<String>(); 523 524 for (String fhirPath : this.pathMapping.keySet()) { 525 switch (this.getTypeOfFhirPath(fhirPath)) { 526 case WHERE_EQUALS: 527 whereEquals.add(fhirPath); 528 break; 529 case WHERE_UNEQUALS: 530 whereUnequals.add(fhirPath); 531 break; 532 case WITHOUT_WHERE: 533 withoutWhere.add(fhirPath); 534 break; 535 } 536 } 537 538 List<String> ret = new ArrayList<String>(); 539 ret.addAll(whereEquals); 540 ret.addAll(whereUnequals); 541 ret.addAll(withoutWhere); 542 return ret; 543 } 544 545 /** 546 * Returns the type of path based on the FHIRPath String. 547 * 548 * @param fhirPath String representation of a FHIRPath. 549 * @return PathType the type of path supplied as `fhirPath`. 550 */ 551 private PathType getTypeOfFhirPath(String fhirPath) { 552 ExpressionNode fhirPathExpression = this.engine.parse(fhirPath); 553 while (fhirPathExpression != null) { 554 if (fhirPathExpression.getKind() == ExpressionNode.Kind.Function) { 555 if (fhirPathExpression.getFunction() == ExpressionNode.Function.Where) { 556 for (ExpressionNode params : fhirPathExpression.getParameters()) { 557 switch (params.getOperation()) { 558 case Equals: 559 return PathType.WHERE_EQUALS; 560 case NotEquals: 561 return PathType.WHERE_UNEQUALS; 562 case And: 563 case As: 564 case Concatenate: 565 case Contains: 566 case Div: 567 case DivideBy: 568 case Equivalent: 569 case Greater: 570 case GreaterOrEqual: 571 case Implies: 572 case In: 573 case Is: 574 case LessOrEqual: 575 case LessThan: 576 case MemberOf: 577 case Minus: 578 case Mod: 579 case NotEquivalent: 580 case Or: 581 case Plus: 582 case Times: 583 case Union: 584 case Xor: 585 // TODO: need to implement above first 586 } 587 } 588 } 589 } 590 fhirPathExpression = fhirPathExpression.getInner(); 591 } 592 return PathType.WITHOUT_WHERE; 593 } 594 595 /** 596 * A simple enum to diffirentiate between types of FHIRPaths in the special use 597 * case of generating FHIR Resources. 598 */ 599 public enum PathType { 600 WHERE_EQUALS, WHERE_UNEQUALS, WITHOUT_WHERE 601 } 602}