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