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}