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