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