
001package org.hl7.fhir.r5.utils.sql; 002 003import java.math.BigDecimal; 004import java.util.ArrayList; 005import java.util.List; 006 007import lombok.extern.slf4j.Slf4j; 008import org.apache.commons.codec.binary.Base64; 009import org.hl7.fhir.exceptions.FHIRException; 010import org.hl7.fhir.exceptions.PathEngineException; 011import org.hl7.fhir.r5.context.IWorkerContext; 012import org.hl7.fhir.r5.elementmodel.Element; 013import org.hl7.fhir.r5.fhirpath.ExpressionNode; 014import org.hl7.fhir.r5.fhirpath.FHIRPathEngine; 015import org.hl7.fhir.r5.fhirpath.TypeDetails; 016import org.hl7.fhir.r5.fhirpath.ExpressionNode.CollectionStatus; 017import org.hl7.fhir.r5.fhirpath.FHIRPathEngine.IEvaluationContext; 018import org.hl7.fhir.r5.fhirpath.FHIRPathUtilityClasses.FunctionDetails; 019import org.hl7.fhir.r5.model.Base; 020import org.hl7.fhir.r5.model.Base64BinaryType; 021import org.hl7.fhir.r5.model.BaseDateTimeType; 022import org.hl7.fhir.r5.model.BooleanType; 023import org.hl7.fhir.r5.model.DecimalType; 024import org.hl7.fhir.r5.model.IntegerType; 025import org.hl7.fhir.r5.model.Property; 026import org.hl7.fhir.r5.model.StringType; 027import org.hl7.fhir.r5.model.ValueSet; 028import org.hl7.fhir.r5.utils.UserDataNames; 029import org.hl7.fhir.utilities.MarkedToMoveToAdjunctPackage; 030import org.hl7.fhir.utilities.json.model.JsonObject; 031import org.hl7.fhir.utilities.validation.ValidationMessage; 032 033 034/** 035 * How to use the Runner: 036 * 037 * create a resource, and fill out: 038 * the context (supports the FHIRPathEngine) 039 * a store that handles the output 040 * a tracker - if you want 041 * 042 * Once it's created, you either run it as a batch, or in trickle mode 043 * 044 * (1) Batch Mode 045 * 046 * * provide a provider 047 * * call execute() with a ViewDefinition 048 * * wait... (watch with an observer if you want to track progress) 049 * 050 * (2) Trickle Mode 051 * * call 'prepare', and keep the WorkContext that's returned 052 * * each time there's a resource to process, call processResource and pass in the workContext and the resource 053 * * when done, call finish(WorkContext) 054 */ 055 056@MarkedToMoveToAdjunctPackage 057@Slf4j 058public class Runner implements IEvaluationContext { 059 060 public interface IRunnerObserver { 061 public void handleRow(Base resource, int total, int cursor); 062 } 063 064 public class WorkContext { 065 private JsonObject vd; 066 private Store store; 067 protected WorkContext(JsonObject vd) { 068 super(); 069 this.vd = vd; 070 } 071 072 } 073 private IWorkerContext context; 074 private Provider provider; 075 private Storage storage; 076 private IRunnerObserver observer; 077 private List<String> prohibitedNames = new ArrayList<String>(); 078 private FHIRPathEngine fpe; 079 080 private String resourceName; 081 private List<ValidationMessage> issues; 082 private int resCount; 083 084 085 public IWorkerContext getContext() { 086 return context; 087 } 088 public void setContext(IWorkerContext context) { 089 this.context = context; 090 } 091 092 public Provider getProvider() { 093 return provider; 094 } 095 public void setProvider(Provider provider) { 096 this.provider = provider; 097 } 098 099 public Storage getStorage() { 100 return storage; 101 } 102 public void setStorage(Storage storage) { 103 this.storage = storage; 104 } 105 106 public List<String> getProhibitedNames() { 107 return prohibitedNames; 108 } 109 110 public void execute(JsonObject viewDefinition) { 111 execute("$", viewDefinition); 112 } 113 114 public void execute(String path, JsonObject viewDefinition) { 115 WorkContext wc = prepare(path, viewDefinition); 116 try { 117 evaluate(wc); 118 } finally { 119 finish(wc); 120 } 121 } 122 123 private void evaluate(WorkContext wc) { 124 List<Base> data = provider.fetch(resourceName); 125 126 int i = 0; 127 for (Base b : data) { 128 if (observer != null) { 129 observer.handleRow(b, data.size(), i); 130 } 131 processResource(wc.vd, wc.store, b); 132 i++; 133 } 134 } 135 136 public WorkContext prepare(String path, JsonObject viewDefinition) { 137 WorkContext wc = new WorkContext(viewDefinition); 138 if (context == null) { 139 throw new FHIRException("No context provided"); 140 } 141 fpe = new FHIRPathEngine(context); 142 fpe.setHostServices(this); 143 fpe.setEmitSQLonFHIRWarning(true); 144 if (viewDefinition == null) { 145 throw new FHIRException("No viewDefinition provided"); 146 } 147 if (provider == null) { 148 throw new FHIRException("No provider provided"); 149 } 150 if (storage == null) { 151 throw new FHIRException("No storage provided"); 152 } 153 Validator validator = new Validator(context, fpe, prohibitedNames, storage.supportsArrays(), storage.supportsComplexTypes(), storage.needsName()); 154 validator.checkViewDefinition(path, viewDefinition); 155 issues = validator.getIssues(); 156 validator.dump(); 157 validator.check(); 158 resourceName = validator.getResourceName(); 159 wc.store = storage.createStore(wc.vd.asString("name"), (List<Column>) wc.vd.getUserData(UserDataNames.db_columns)); 160 return wc; 161 } 162 163 public void processResource(WorkContext wc, Base b) { 164 if (observer != null) { 165 observer.handleRow(b, -1, resCount); 166 } 167 processResource(wc.vd, wc.store, b); 168 resCount++; 169 wc.store.flush(); 170 } 171 172 private void processResource(JsonObject vd, Store store, Base b) { 173 boolean ok = true; 174 for (JsonObject w : vd.getJsonObjects("where")) { 175 String expr = w.asString("path"); 176 ExpressionNode node = fpe.parse(expr); 177 boolean pass = fpe.evaluateToBoolean(vd, b, b, b, node); 178 if (!pass) { 179 ok = false; 180 break; 181 } 182 } 183 if (ok) { 184 List<List<Cell>> rows = new ArrayList<>(); 185 rows.add(new ArrayList<Cell>()); 186 187 for (JsonObject select : vd.getJsonObjects("select")) { 188 executeSelect(vd, select, b, rows); 189 } 190 for (List<Cell> row : rows) { 191 storage.addRow(store, row); 192 } 193 } 194 } 195 196 public void finish(WorkContext wc) { 197 storage.finish(wc.store); 198 } 199 200 private void executeSelect(JsonObject vd, JsonObject select, Base b, List<List<Cell>> rows) { 201 List<Base> focus = new ArrayList<>(); 202 203 if (select.has("forEach")) { 204 focus.addAll(executeForEach(vd, select, b)); 205 } else if (select.has("forEachOrNull")) { 206 207 focus.addAll(executeForEachOrNull(vd, select, b)); 208 if (focus.isEmpty()) { 209 List<Column> columns = (List<Column>) select.getUserData(UserDataNames.db_columns); 210 for (List<Cell> row : rows) { 211 for (Column c : columns) { 212 Cell cell = cell(row, c.getName()); 213 if (cell == null) { 214 row.add(new Cell(c, null)); 215 } 216 } 217 } 218 return; 219 } 220 } else { 221 focus.add(b); 222 } 223 224 // } else if (select.has("unionAll")) { 225 // focus.addAll(executeUnion(select, b)); 226 227 List<List<Cell>> tempRows = new ArrayList<>(); 228 tempRows.addAll(rows); 229 rows.clear(); 230 231 for (Base f : focus) { 232 List<List<Cell>> rowsToAdd = cloneRows(tempRows); 233 234 for (JsonObject column : select.getJsonObjects("column")) { 235 executeColumn(vd, column, f, rowsToAdd); 236 } 237 238 for (JsonObject sub : select.getJsonObjects("select")) { 239 executeSelect(vd, sub, f, rowsToAdd); 240 } 241 242 executeUnionAll(vd, select.getJsonObjects("unionAll"), f, rowsToAdd); 243 244 rows.addAll(rowsToAdd); 245 } 246 } 247 248 private void executeUnionAll(JsonObject vd, List<JsonObject> unionList, Base b, List<List<Cell>> rows) { 249 if (unionList.isEmpty()) { 250 return; 251 } 252 List<List<Cell>> sourceRows = new ArrayList<>(); 253 sourceRows.addAll(rows); 254 rows.clear(); 255 256 for (JsonObject union : unionList) { 257 List<List<Cell>> tempRows = new ArrayList<>(); 258 tempRows.addAll(sourceRows); 259 executeSelect(vd, union, b, tempRows); 260 rows.addAll(tempRows); 261 } 262 } 263 264 private List<List<Cell>> cloneRows(List<List<Cell>> rows) { 265 List<List<Cell>> list = new ArrayList<>(); 266 for (List<Cell> row : rows) { 267 list.add(cloneRow(row)); 268 } 269 return list; 270 } 271 272 private List<Cell> cloneRow(List<Cell> cells) { 273 List<Cell> list = new ArrayList<>(); 274 for (Cell cell : cells) { 275 list.add(cell.copy()); 276 } 277 return list; 278 } 279 280 private List<Base> executeForEach(JsonObject vd, JsonObject focus, Base b) { 281 ExpressionNode n = (ExpressionNode) focus.getUserData(UserDataNames.db_forEach); 282 List<Base> result = new ArrayList<>(); 283 result.addAll(fpe.evaluate(vd, b, n)); 284 return result; 285 } 286 287 private List<Base> executeForEachOrNull(JsonObject vd, JsonObject focus, Base b) { 288 ExpressionNode n = (ExpressionNode) focus.getUserData(UserDataNames.db_forEachOrNull); 289 List<Base> result = new ArrayList<>(); 290 result.addAll(fpe.evaluate(vd, b, n)); 291 return result; 292 } 293 294 private void executeColumn(JsonObject vd, JsonObject column, Base b, List<List<Cell>> rows) { 295 ExpressionNode n = (ExpressionNode) column.getUserData(UserDataNames.db_path); 296 List<Base> bl2 = new ArrayList<>(); 297 if (b != null) { 298 bl2.addAll(fpe.evaluate(vd, b, n)); 299 } 300 Column col = (Column) column.getUserData(UserDataNames.db_column); 301 if (col == null) { 302 log.error("Error"); 303 } else { 304 for (List<Cell> row : rows) { 305 Cell c = cell(row, col.getName()); 306 if (c == null) { 307 c = new Cell(col); 308 row.add(c); 309 } 310 if (!bl2.isEmpty()) { 311 if (bl2.size() + c.getValues().size() > 1) { 312 // this is a problem if collection != true or if the storage can't deal with it 313 // though this should've been picked up before now - but there are circumstances where it wouldn't be 314 if (!c.getColumn().isColl()) { 315 throw new FHIRException("The column "+c.getColumn().getName()+" is not allowed multiple values, but at least one row has multiple values"); 316 } 317 } 318 for (Base b2 : bl2) { 319 c.getValues().add(genValue(c.getColumn(), b2)); 320 } 321 } 322 } 323 } 324 } 325 326 327 private Value genValue(Column column, Base b) { 328 if (column.getKind() == null) { 329 throw new FHIRException("Attempt to add a type "+b.fhirType()+" to an unknown column type (null) for column "+column.getName()); // can't happen 330 } 331 switch (column.getKind()) { 332 case Binary: 333 if (b instanceof Base64BinaryType) { 334 Base64BinaryType bb = (Base64BinaryType) b; 335 return Value.makeBinary(bb.primitiveValue(), bb.getValue()); 336 } else if (b.isBooleanPrimitive()) { // ElementModel 337 return Value.makeBinary(b.primitiveValue(), Base64.decodeBase64(b.primitiveValue())); 338 } else { 339 throw new FHIRException("Attempt to add a type "+b.fhirType()+" to a binary column for column "+column.getName()); 340 } 341 case Boolean: 342 if (b instanceof BooleanType) { 343 BooleanType bb = (BooleanType) b; 344 return Value.makeBoolean(bb.primitiveValue(), bb.booleanValue()); 345 } else if (b.isBooleanPrimitive()) { // ElementModel 346 return Value.makeBoolean(b.primitiveValue(), "true".equals(b.primitiveValue())); 347 } else { 348 throw new FHIRException("Attempt to add a type "+b.fhirType()+" to a boolean column for column "+column.getName()); 349 } 350 case Complex: 351 if (b.isPrimitive()) { 352 throw new FHIRException("Attempt to add a primitive type "+b.fhirType()+" to a complex column for column "+column.getName()); 353 } else { 354 return Value.makeComplex(b); 355 } 356 case DateTime: 357 if (b instanceof BaseDateTimeType) { 358 BaseDateTimeType d = (BaseDateTimeType) b; 359 return Value.makeDate(d.primitiveValue(), d.getValue()); 360 } else if (b.isPrimitive() && b.isDateTime()) { // ElementModel 361 return Value.makeDate(b.primitiveValue(), b.dateTimeValue().getValue()); 362 } else { 363 throw new FHIRException("Attempt to add a type "+b.fhirType()+" to an integer column for column "+column.getName()); 364 } 365 case Decimal: 366 if (b instanceof DecimalType) { 367 DecimalType d = (DecimalType) b; 368 return Value.makeDecimal(d.primitiveValue(), d.getValue()); 369 } else if (b.isPrimitive()) { // ElementModel 370 return Value.makeDecimal(b.primitiveValue(), new BigDecimal(b.primitiveValue())); 371 } else { 372 throw new FHIRException("Attempt to add a type "+b.fhirType()+" to an integer column for column "+column.getName()); 373 } 374 case Integer: 375 if (b instanceof IntegerType) { 376 IntegerType i = (IntegerType) b; 377 return Value.makeInteger(i.primitiveValue(), i.getValue()); 378 } else if (b.isPrimitive()) { // ElementModel 379 return Value.makeInteger(b.primitiveValue(), Integer.valueOf(b.primitiveValue())); 380 } else { 381 throw new FHIRException("Attempt to add a type "+b.fhirType()+" to an integer column for column "+column.getName()); 382 } 383 case String: 384 if (b.isPrimitive()) { 385 return Value.makeString(b.primitiveValue()); 386 } else { 387 throw new FHIRException("Attempt to add a complex type "+b.fhirType()+" to a string column for column "+column.getName()); 388 } 389 case Time: 390 if (b.fhirType().equals("time")) { 391 return Value.makeString(b.primitiveValue()); 392 } else { 393 throw new FHIRException("Attempt to add a type "+b.fhirType()+" to a time column for column "+column.getName()); 394 } 395 default: 396 throw new FHIRException("Attempt to add a type "+b.fhirType()+" to an unknown column type for column "+column.getName()); 397 } 398 } 399 400 private Column column(String columnName, List<Column> columns) { 401 for (Column t : columns) { 402 if (t.getName().equalsIgnoreCase(columnName)) { 403 return t; 404 } 405 } 406 return null; 407 } 408 409 private Cell cell(List<Cell> cells, String columnName) { 410 for (Cell t : cells) { 411 if (t.getColumn().getName().equalsIgnoreCase(columnName)) { 412 return t; 413 } 414 } 415 return null; 416 } 417 418 @Override 419 public List<Base> resolveConstant(FHIRPathEngine engine, Object appContext, String name, boolean beforeContext, boolean explicitConstant) throws PathEngineException { 420 List<Base> list = new ArrayList<Base>(); 421 if (explicitConstant) { 422 JsonObject vd = (JsonObject) appContext; 423 JsonObject constant = findConstant(vd, name); 424 if (constant != null) { 425 Base b = (Base) constant.getUserData(UserDataNames.db_value); 426 if (b != null) { 427 list.add(b); 428 } 429 } 430 } 431 return list; 432 } 433 434 @Override 435 public TypeDetails resolveConstantType(FHIRPathEngine engine, Object appContext, String name, boolean explicitConstant) throws PathEngineException { 436 if (explicitConstant) { 437 if (appContext instanceof JsonObject) { 438 JsonObject vd = (JsonObject) appContext; 439 JsonObject constant = findConstant(vd, name.substring(1)); 440 if (constant != null) { 441 Base b = (Base) constant.getUserData(UserDataNames.db_value); 442 if (b != null) { 443 return new TypeDetails(CollectionStatus.SINGLETON, b.fhirType()); 444 } 445 } 446 } else if (appContext instanceof Element) { 447 Element vd = (Element) appContext; 448 Element constant = findConstant(vd, name.substring(1)); 449 if (constant != null) { 450 Element v = constant.getNamedChild("value"); 451 if (v != null) { 452 return new TypeDetails(CollectionStatus.SINGLETON, v.fhirType()); 453 } 454 } 455 } 456 } 457 return null; 458 } 459 460 private JsonObject findConstant(JsonObject vd, String name) { 461 for (JsonObject o : vd.getJsonObjects("constant")) { 462 if (name.equals(o.asString("name"))) { 463 return o; 464 } 465 } 466 return null; 467 } 468 469 private Element findConstant(Element vd, String name) { 470 for (Element o : vd.getChildren("constant")) { 471 if (name.equals(o.getNamedChildValue("name"))) { 472 return o; 473 } 474 } 475 return null; 476 } 477 478 @Override 479 public boolean log(String argument, List<Base> focus) { 480 throw new Error("Not implemented yet: log"); 481 } 482 483 @Override 484 public FunctionDetails resolveFunction(FHIRPathEngine engine, String functionName) { 485 switch (functionName) { 486 case "getResourceKey" : return new FunctionDetails("Unique Key for resource", 0, 0); 487 case "getReferenceKey" : return new FunctionDetails("Unique Key for resource that is the target of the reference", 0, 1); 488 default: return null; 489 } 490 } 491 @Override 492 public TypeDetails checkFunction(FHIRPathEngine engine, Object appContext, String functionName, TypeDetails focus, List<TypeDetails> parameters) throws PathEngineException { 493 switch (functionName) { 494 case "getResourceKey" : return new TypeDetails(CollectionStatus.SINGLETON, "string"); 495 case "getReferenceKey" : return new TypeDetails(CollectionStatus.SINGLETON, "string"); 496 default: throw new Error("Not known: "+functionName); 497 } 498 } 499 500 @Override 501 public List<Base> executeFunction(FHIRPathEngine engine, Object appContext, List<Base> focus, String functionName, List<List<Base>> parameters) { 502 switch (functionName) { 503 case "getResourceKey" : return executeResourceKey(focus); 504 case "getReferenceKey" : return executeReferenceKey(null, focus, parameters); 505 default: throw new Error("Not known: "+functionName); 506 } 507 } 508 509 private List<Base> executeResourceKey(List<Base> focus) { 510 List<Base> base = new ArrayList<Base>(); 511 if (focus.size() == 1) { 512 Base res = focus.get(0); 513 if (!res.hasUserData(UserDataNames.Storage_key)) { 514 String key = storage.getKeyForSourceResource(res); 515 if (key == null) { 516 throw new FHIRException("Unidentified resource: "+res.fhirType()+"/"+res.getIdBase()); 517 } else { 518 res.setUserData(UserDataNames.Storage_key, key); 519 } 520 } 521 base.add(new StringType(res.getUserString(UserDataNames.Storage_key))); 522 } 523 return base; 524 } 525 526 private List<Base> executeReferenceKey(Base rootResource, List<Base> focus, List<List<Base>> parameters) { 527 String rt = null; 528 if (parameters.size() > 0) { 529 rt = parameters.get(0).get(0).primitiveValue(); 530 if (rt.startsWith("FHIR.")) { 531 rt = rt.substring(5); 532 } 533 } 534 List<Base> base = new ArrayList<Base>(); 535 if (focus.size() == 1) { 536 Base res = focus.get(0); 537 String ref = null; 538 if (res.fhirType().equals("Reference")) { 539 ref = getRef(res); 540 } else if (res.isPrimitive()) { 541 ref = res.primitiveValue(); 542 } else { 543 throw new FHIRException("Unable to generate a reference key based on a "+res.fhirType()); 544 } 545 if (ref != null) { 546 Base target = provider.resolveReference(rootResource, ref, rt); 547 if (target != null) { 548 if (!res.hasUserData(UserDataNames.Storage_key)) { 549 String key = storage.getKeyForTargetResource(target); 550 if (key == null) { 551 throw new FHIRException("Unidentified resource: "+res.fhirType()+"/"+res.getIdBase()); 552 } else { 553 res.setUserData(UserDataNames.Storage_key, key); 554 } 555 } 556 base.add(new StringType(res.getUserString(UserDataNames.Storage_key))); 557 } 558 } 559 } 560 return base; 561 } 562 563 private String getRef(Base res) { 564 Property prop = res.getChildByName("reference"); 565 if (prop != null && prop.getValues().size() == 1) { 566 return prop.getValues().get(0).primitiveValue(); 567 } 568 return null; 569 } 570 571 @Override 572 public Base resolveReference(FHIRPathEngine engine, Object appContext, String url, Base refContext) throws FHIRException { 573 throw new Error("Not implemented yet: resolveReference"); 574 } 575 576 @Override 577 public boolean conformsToProfile(FHIRPathEngine engine, Object appContext, Base item, String url) throws FHIRException { 578 throw new Error("Not implemented yet: conformsToProfile"); 579 } 580 581 @Override 582 public ValueSet resolveValueSet(FHIRPathEngine engine, Object appContext, String url) { 583 throw new Error("Not implemented yet: resolveValueSet"); 584 } 585 @Override 586 public boolean paramIsType(String name, int index) { 587 return "getReferenceKey".equals(name); 588 } 589 public List<ValidationMessage> getIssues() { 590 return issues; 591 } 592 593 594}