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}