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}