diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/sql/Cell.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/sql/Cell.java new file mode 100644 index 000000000..0e8e37f9a --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/sql/Cell.java @@ -0,0 +1,33 @@ +package org.hl7.fhir.r5.utils.sql; + +import java.util.ArrayList; +import java.util.List; + + +public class Cell { + private Column column; + private List values = new ArrayList<>(); + + public Cell(Column column) { + super(); + this.column = column; + } + + public Column getColumn() { + return column; + } + + public List getValues() { + return values; + } + + public Cell copy() { + Cell cell = new Cell(column); + for (Value v : values) { + cell.values.add(v); // values are immutable, so we don't need to clone them + } + return cell; + } + + +} diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/sql/Column.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/sql/Column.java new file mode 100644 index 000000000..abddd4664 --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/sql/Column.java @@ -0,0 +1,62 @@ +package org.hl7.fhir.r5.utils.sql; + +public class Column { + + private String name; + private int length; + private String type; + private ColumnKind kind; + private boolean isColl; + + protected Column() { + super(); + } + + protected Column(String name, boolean isColl, String type, ColumnKind kind) { + super(); + this.name = name; + this.isColl = isColl; + this.type = type; + this.kind = kind; + } + + public String getName() { + return name; + } + public int getLength() { + return length; + } + public ColumnKind getKind() { + return kind; + } + + public void setName(String name) { + this.name = name; + } + + public void setLength(int length) { + this.length = length; + } + + public void setKind(ColumnKind kind) { + this.kind = kind; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public boolean isColl() { + return isColl; + } + + public void setColl(boolean isColl) { + this.isColl = isColl; + } + + +} diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/sql/ColumnKind.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/sql/ColumnKind.java new file mode 100644 index 000000000..a1733f1d6 --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/sql/ColumnKind.java @@ -0,0 +1,5 @@ +package org.hl7.fhir.r5.utils.sql; + +public enum ColumnKind { + String, DateTime, Integer, Decimal, Binary, Time, Boolean, Complex +} diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/sql/Provider.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/sql/Provider.java new file mode 100644 index 000000000..d1aeeaabb --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/sql/Provider.java @@ -0,0 +1,11 @@ +package org.hl7.fhir.r5.utils.sql; + +import java.util.List; + +import org.hl7.fhir.r5.model.Base; + +public interface Provider { + List fetch(String resourceType); + + Base resolveReference(String ref, String resourceType); +} diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/sql/Runner.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/sql/Runner.java new file mode 100644 index 000000000..ad34b998f --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/sql/Runner.java @@ -0,0 +1,431 @@ +package org.hl7.fhir.r5.utils.sql; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.net.util.Base64; +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.exceptions.PathEngineException; +import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.r5.model.Base; +import org.hl7.fhir.r5.model.Base64BinaryType; +import org.hl7.fhir.r5.model.BaseDateTimeType; +import org.hl7.fhir.r5.model.BooleanType; +import org.hl7.fhir.r5.model.DecimalType; +import org.hl7.fhir.r5.model.ExpressionNode; +import org.hl7.fhir.r5.model.ExpressionNode.CollectionStatus; +import org.hl7.fhir.r5.model.IntegerType; +import org.hl7.fhir.r5.model.Property; +import org.hl7.fhir.r5.model.StringType; +import org.hl7.fhir.r5.model.TypeDetails; +import org.hl7.fhir.r5.model.ValueSet; +import org.hl7.fhir.r5.utils.FHIRPathEngine; +import org.hl7.fhir.r5.utils.FHIRPathEngine.IEvaluationContext; +import org.hl7.fhir.r5.utils.FHIRPathUtilityClasses.FunctionDetails; +import org.hl7.fhir.utilities.json.model.JsonObject; + + +public class Runner implements IEvaluationContext { + private IWorkerContext context; + private Provider provider; + private Storage storage; + private List prohibitedNames = new ArrayList(); + private FHIRPathEngine fpe; + + private List columns = new ArrayList<>(); + + private String resourceName; + + + public IWorkerContext getContext() { + return context; + } + public void setContext(IWorkerContext context) { + this.context = context; + } + + + public Provider getProvider() { + return provider; + } + public void setProvider(Provider provider) { + this.provider = provider; + } + + public Storage getStorage() { + return storage; + } + public void setStorage(Storage storage) { + this.storage = storage; + } + + public List getProhibitedNames() { + return prohibitedNames; + } + + public void execute(JsonObject viewDefinition) { + execute("$", viewDefinition); + } + + public void execute(String path, JsonObject viewDefinition) { + if (context == null) { + throw new FHIRException("No context provided"); + } + fpe = new FHIRPathEngine(context); + fpe.setHostServices(this); + if (viewDefinition == null) { + throw new FHIRException("No viewDefinition provided"); + } + if (provider == null) { + throw new FHIRException("No provider provided"); + } + if (storage == null) { + throw new FHIRException("No storage provided"); + } + Validator validator = new Validator(context, fpe, prohibitedNames, storage.supportsArrays(), storage.supportsComplexTypes(), storage.needsName()); + validator.checkViewDefinition(path, viewDefinition); + validator.dump(); + validator.check(); + resourceName = validator.getResourceName(); + columns = validator.getColumns(); + evaluate(viewDefinition); + } + + private void evaluate(JsonObject vd) { + Store store = storage.createStore(vd.asString("name"), columns); + + List data = provider.fetch(resourceName); + + for (Base b : data) { + boolean ok = true; + for (JsonObject w : vd.getJsonObjects("where")) { + String expr = w.asString("path"); + ExpressionNode node = fpe.parse(expr); + boolean pass = fpe.evaluateToBoolean(null, b, b, b, node); + if (!pass) { + ok = false; + break; + } + } + if (ok) { + List> rows = new ArrayList<>(); + generateCells(b, vd, rows); + for (List row : rows) { + storage.addRow(store, row); + } + } + } + storage.finish(store); + } + + private void generateCells(Base bl, JsonObject vd, List> rows) { + if (vd.has("forEach")) { + executeForEach(vd, bl, rows); + } else if (vd.has("forEachOrNull")) { + executeForEachOrNull(vd, bl, rows); + } else if (vd.has("union")) { + executeUnion(vd, bl, rows); + } else { + for (JsonObject select : vd.getJsonObjects("select")) { + executeSelect(select, bl, rows); + } + } + } + + private void executeSelect(JsonObject select, Base bl, List> rows) { + if (select.has("path")) { + executeSelectPath(select, bl, rows); + } else if (select.has("forEach")) { + executeForEach(select, bl, rows); + } else if (select.has("forEachOrNull")) { + executeForEachOrNull(select, bl, rows); + } else if (select.has("union")) { + executeUnion(select, bl, rows); + } + } + + private void executeForEach(JsonObject focus, Base b, List> rows) { + ExpressionNode n = (ExpressionNode) focus.getUserData("forEach"); + List bl2 = fpe.evaluate(b, n); + List> tempRows = new ArrayList<>(); + tempRows.addAll(rows); + rows.clear(); + for (Base b2 : bl2) { + List> rowsToAdd = cloneRows(tempRows); + for (JsonObject select : focus.getJsonObjects("select")) { + executeSelect(select, b2, rowsToAdd); + } + rows.addAll(rowsToAdd); + } + } + + private List> cloneRows(List> rows) { + List> list = new ArrayList<>(); + for (List row : rows) { + list.add(cloneRow(row)); + } + return list; + } + + private List cloneRow(List cells) { + List list = new ArrayList<>(); + for (Cell cell : cells) { + list.add(cell.copy()); + } + return list; + } + + private void executeForEachOrNull(JsonObject focus, Base b, List> rows) { + throw new FHIRException("forEachOrNull is not supported"); + } + + private void executeUnion(JsonObject focus, Base b, List> rows) { + throw new FHIRException("union is not supported"); + } + + + private void executeSelectPath(JsonObject select, Base b, List> rows) { + ExpressionNode n = (ExpressionNode) select.getUserData("path"); + List bl2 = fpe.evaluate(b, n); + String name = select.getUserString("name"); + if (!bl2.isEmpty()) { + if (rows.isEmpty()) { + rows.add(new ArrayList()); + } + for (List row : rows) { + Cell c = cell(row, name); + if (c == null) { + c = new Cell(column(name)); + row.add(c); + } + if (bl2.size() + c.getValues().size() > 1) { + // this is a problem if collection != true or if the storage can't deal with it + // though this should've been picked up before now - but there are circumstances where it wouldn't be + if (!c.getColumn().isColl()) { + throw new FHIRException("The column "+c.getColumn().getName()+" is not allowed multiple values, but at least one row has multiple values"); + } + } + for (Base b2 : bl2) { + c.getValues().add(genValue(c.getColumn(), b2)); + } + } + } else { + for (List row : rows) { + Cell c = cell(row, name); + if (c == null) { + c = new Cell(column(name)); + row.add(c); + } + } + } + } + + + private Value genValue(Column column, Base b) { + if (column.getKind() == null) { + throw new FHIRException("Attempt to add a type "+b.fhirType()+" to an unknown column type (null) for column "+column.getName()); // can't happen + } + switch (column.getKind()) { + case Binary: + if (b instanceof Base64BinaryType) { + Base64BinaryType bb = (Base64BinaryType) b; + return Value.makeBinary(bb.primitiveValue(), bb.getValue()); + } else if (b.isBooleanPrimitive()) { // ElementModel + return Value.makeBinary(b.primitiveValue(), Base64.decodeBase64(b.primitiveValue())); + } else { + throw new FHIRException("Attempt to add a type "+b.fhirType()+" to a binary column for column "+column.getName()); + } + case Boolean: + if (b instanceof BooleanType) { + BooleanType bb = (BooleanType) b; + return Value.makeBoolean(bb.primitiveValue(), bb.booleanValue()); + } else if (b.isBooleanPrimitive()) { // ElementModel + return Value.makeBoolean(b.primitiveValue(), "true".equals(b.primitiveValue())); + } else { + throw new FHIRException("Attempt to add a type "+b.fhirType()+" to a boolean column for column "+column.getName()); + } + case Complex: + if (b.isPrimitive()) { + throw new FHIRException("Attempt to add a primitive type "+b.fhirType()+" to a complex column for column "+column.getName()); + } else { + return Value.makeComplex(b); + } + case DateTime: + if (b instanceof BaseDateTimeType) { + BaseDateTimeType d = (BaseDateTimeType) b; + return Value.makeDate(d.primitiveValue(), d.getValue()); + } else if (b.isPrimitive() && b.isDateTime()) { // ElementModel + return Value.makeDate(b.primitiveValue(), b.dateTimeValue().getValue()); + } else { + throw new FHIRException("Attempt to add a type "+b.fhirType()+" to an integer column for column "+column.getName()); + } + case Decimal: + if (b instanceof DecimalType) { + DecimalType d = (DecimalType) b; + return Value.makeDecimal(d.primitiveValue(), d.getValue()); + } else if (b.isPrimitive()) { // ElementModel + return Value.makeDecimal(b.primitiveValue(), new BigDecimal(b.primitiveValue())); + } else { + throw new FHIRException("Attempt to add a type "+b.fhirType()+" to an integer column for column "+column.getName()); + } + case Integer: + if (b instanceof IntegerType) { + IntegerType i = (IntegerType) b; + return Value.makeInteger(i.primitiveValue(), i.getValue()); + } else if (b.isPrimitive()) { // ElementModel + return Value.makeInteger(b.primitiveValue(), Integer.valueOf(b.primitiveValue())); + } else { + throw new FHIRException("Attempt to add a type "+b.fhirType()+" to an integer column for column "+column.getName()); + } + case String: + if (b.isPrimitive()) { + return Value.makeString(b.primitiveValue()); + } else { + throw new FHIRException("Attempt to add a complex type "+b.fhirType()+" to a string column for column "+column.getName()); + } + case Time: + if (b.fhirType().equals("time")) { + return Value.makeString(b.primitiveValue()); + } else { + throw new FHIRException("Attempt to add a type "+b.fhirType()+" to a time column for column "+column.getName()); + } + default: + throw new FHIRException("Attempt to add a type "+b.fhirType()+" to an unknown column type for column "+column.getName()); + } + } + + private Column column(String columnName) { + for (Column t : columns) { + if (t.getName().equalsIgnoreCase(columnName)) { + return t; + } + } + return null; + } + + private Cell cell(List cells, String columnName) { + for (Cell t : cells) { + if (t.getColumn().getName().equalsIgnoreCase(columnName)) { + return t; + } + } + return null; + } + + @Override + public List resolveConstant(Object appContext, String name, boolean beforeContext) throws PathEngineException { + throw new Error("Not implemented yet: resolveConstant"); + } + + @Override + public TypeDetails resolveConstantType(Object appContext, String name) throws PathEngineException { + throw new Error("Not implemented yet: resolveConstantType"); + } + + @Override + public boolean log(String argument, List focus) { + throw new Error("Not implemented yet: log"); + } + + @Override + public FunctionDetails resolveFunction(String functionName) { + switch (functionName) { + case "getResourceKey" : return new FunctionDetails("Unique Key for resource", 0, 0); + case "getReferenceKey" : return new FunctionDetails("Unique Key for resource that is the target of the reference", 0, 1); + default: return null; + } + } + @Override + public TypeDetails checkFunction(Object appContext, String functionName, List parameters) throws PathEngineException { + switch (functionName) { + case "getResourceKey" : return new TypeDetails(CollectionStatus.SINGLETON, "string"); + case "getReferenceKey" : return new TypeDetails(CollectionStatus.SINGLETON, "string"); + default: throw new Error("Not known: "+functionName); + } + } + + @Override + public List executeFunction(Object appContext, List focus, String functionName, List> parameters) { + switch (functionName) { + case "getResourceKey" : return executeResourceKey(focus); + case "getReferenceKey" : return executeReferenceKey(focus, parameters); + default: throw new Error("Not known: "+functionName); + } + } + + private List executeResourceKey(List focus) { + List base = new ArrayList(); + if (focus.size() == 1) { + Base res = focus.get(0); + if (!res.hasUserData("Storage.key")) { + String key = storage.getKeyForSourceResource(res); + if (key == null) { + throw new FHIRException("Unidentified resource: "+res.fhirType()+"/"+res.getIdBase()); + } else { + res.setUserData("Storage.key", key); + } + } + base.add(new StringType(res.getUserString("Storage.key"))); + } + return base; + } + + private List executeReferenceKey(List focus, List> parameters) { + String rt = null; + if (parameters.size() > 0) { + rt = parameters.get(0).get(0).primitiveValue(); + } + List base = new ArrayList(); + if (focus.size() == 1) { + Base res = focus.get(0); + String ref = null; + if (res.fhirType().equals("Reference")) { + ref = getRef(res); + } else if (res.isPrimitive()) { + ref = res.primitiveValue(); + } else { + throw new FHIRException("Unable to generate a reference key based on a "+res.fhirType()); + } + if (ref != null) { + Base target = provider.resolveReference(ref, rt); + if (target != null) { + if (!res.hasUserData("Storage.key")) { + String key = storage.getKeyForTargetResource(target); + if (key == null) { + throw new FHIRException("Unidentified resource: "+res.fhirType()+"/"+res.getIdBase()); + } else { + res.setUserData("Storage.key", key); + } + } + base.add(new StringType(res.getUserString("Storage.key"))); + } + } + } + return base; + } + + private String getRef(Base res) { + Property prop = res.getChildByName("reference"); + if (prop != null && prop.getValues().size() == 1) { + return prop.getValues().get(0).primitiveValue(); + } + return null; + } + @Override + public Base resolveReference(Object appContext, String url, Base refContext) throws FHIRException { + throw new Error("Not implemented yet: resolveReference"); + } + + @Override + public boolean conformsToProfile(Object appContext, Base item, String url) throws FHIRException { + throw new Error("Not implemented yet: conformsToProfile"); + } + + @Override + public ValueSet resolveValueSet(Object appContext, String url) { + throw new Error("Not implemented yet: resolveValueSet"); + } + + +} diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/sql/Storage.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/sql/Storage.java new file mode 100644 index 000000000..41740d108 --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/sql/Storage.java @@ -0,0 +1,18 @@ +package org.hl7.fhir.r5.utils.sql; + +import java.util.List; + +import org.hl7.fhir.r5.model.Base; + +public interface Storage { + + boolean supportsArrays(); + boolean supportsComplexTypes(); + + Store createStore(String name, List columns); + void addRow(Store store, List cells); + void finish(Store store); + boolean needsName(); + String getKeyForSourceResource(Base res); + String getKeyForTargetResource(Base res); +} diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/sql/StorageJson.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/sql/StorageJson.java new file mode 100644 index 000000000..9aca2cb22 --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/sql/StorageJson.java @@ -0,0 +1,96 @@ +package org.hl7.fhir.r5.utils.sql; + +import java.util.List; + +import org.hl7.fhir.r5.model.Base; +import org.hl7.fhir.utilities.json.model.JsonArray; +import org.hl7.fhir.utilities.json.model.JsonBoolean; +import org.hl7.fhir.utilities.json.model.JsonElement; +import org.hl7.fhir.utilities.json.model.JsonNull; +import org.hl7.fhir.utilities.json.model.JsonNumber; +import org.hl7.fhir.utilities.json.model.JsonObject; +import org.hl7.fhir.utilities.json.model.JsonString; + +public class StorageJson implements Storage { + + private String name; + private JsonArray rows; + + @Override + public boolean supportsArrays() { + return true; + } + + @Override + public Store createStore(String name, List columns) { + this.name = name; + this.rows = new JsonArray(); + return new Store(name); // we're not doing anything with this + } + + @Override + public void addRow(Store store, List cells) { + JsonObject row = new JsonObject(); + rows.add(row); + for (Cell cell : cells) { + if (cell.getValues().size() == 0) { + row.add(cell.getColumn().getName(), new JsonNull()); + } else if (cell.getValues().size() == 1) { + row.add(cell.getColumn().getName(), makeJsonNode(cell.getValues().get(0))); + } else { + JsonArray arr = new JsonArray(); + row.add(cell.getColumn().getName(), arr); + for (Value value : cell.getValues()) { + arr.add(makeJsonNode(value)); + } + } + } + } + + private JsonElement makeJsonNode(Value value) { + if (value.getValueInt() != null) { + return new JsonNumber(value.getValueInt().intValue()); + } + if (value.getValueBoolean() != null) { + return new JsonBoolean(value.getValueBoolean().booleanValue()); + } + if (value.getValueDecimal() != null) { + return new JsonNumber(value.getValueDecimal().toPlainString()); + } + return new JsonString(value.getValueString()); + } + + @Override + public void finish(Store store) { + // nothing + } + + public String getName() { + return name; + } + + public JsonArray getRows() { + return rows; + } + + @Override + public boolean supportsComplexTypes() { + return true; + } + + @Override + public boolean needsName() { + return false; + } + + @Override + public String getKeyForSourceResource(Base res) { + return res.getIdBase(); + } + + @Override + public String getKeyForTargetResource(Base res) { + return res.fhirType()+"/"+res.getIdBase(); + } + +} diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/sql/StorageSqlite3.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/sql/StorageSqlite3.java new file mode 100644 index 000000000..0332e17fd --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/sql/StorageSqlite3.java @@ -0,0 +1,141 @@ +package org.hl7.fhir.r5.utils.sql; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.util.List; + +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.r5.model.Base; +import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; + +public class StorageSqlite3 implements Storage { + + public static class SQLiteStore extends Store { + private PreparedStatement p; + + protected SQLiteStore(String name, PreparedStatement p) { + super(name); + this.p = p; + } + + public PreparedStatement getP() { + return p; + } + + } + + private Connection conn; + private int nextKey = 0; + + protected StorageSqlite3(Connection conn) { + super(); + this.conn = conn; + } + + @Override + public Store createStore(String name, List columns) { + try { + CommaSeparatedStringBuilder fields = new CommaSeparatedStringBuilder(", "); + CommaSeparatedStringBuilder values = new CommaSeparatedStringBuilder(", "); + StringBuilder b = new StringBuilder(); + b.append("Create Table "+name+" { "); + b.append("ViewRowKey integer NOT NULL"); + for (Column column : columns) { + b.append(", "+column.getName()+" "+sqliteType(column.getKind())+" NULL"); // index columns are always nullable + fields.append(column.getName()); + values.append("?"); + } + b.append(", PRIMARY KEY (ViewRowKey))\r\n"); + conn.createStatement().execute(b.toString()); + + String isql = "Insert into "+name+" ("+fields.toString()+") values ("+values.toString()+")"; + PreparedStatement psql = conn.prepareStatement(isql); + return new SQLiteStore(name, psql); + } catch (Exception e) { + throw new FHIRException(e); + } + } + + private String sqliteType(ColumnKind type) { + switch (type) { + case DateTime: return "Text"; + case Decimal: return "Real"; + case Integer: return "Integer"; + case String: return "Text"; + case Time: return "Text"; + case Binary: return "Text"; + case Boolean: return "Integer"; + case Complex: throw new FHIRException("SQLite runner does not handle complexes"); + } + return null; + } + + @Override + public void addRow(Store store, List cells) { + try { + SQLiteStore sqls = (SQLiteStore) store; + PreparedStatement p = sqls.getP(); + p.setInt(1, ++nextKey); + for (int i = 0; i < cells.size(); i++) { + Cell c = cells.get(i); + switch (c.getColumn().getKind()) { + case Binary: + p.setBytes(i+2, c.getValues().size() == 0 ? null : c.getValues().get(0).getValueBinary()); + break; + case Boolean: + p.setBoolean(i+2, c.getValues().size() == 0 ? false : c.getValues().get(0).getValueBoolean().booleanValue()); + break; + case DateTime: + p.setDate(i+2, c.getValues().size() == 0 ? null : new java.sql.Date(c.getValues().get(0).getValueDate().getTime())); + break; + case Decimal: + p.setString(i+2, c.getValues().size() == 0 ? null : c.getValues().get(0).getValueString()); + break; + case Integer: + p.setInt(i+2, c.getValues().size() == 0 ? 0 : c.getValues().get(0).getValueInt().intValue()); + break; + case String: + p.setString(i+2, c.getValues().size() == 0 ? null : c.getValues().get(0).getValueString()); + break; + case Time: + p.setString(i+2, c.getValues().size() == 0 ? null : c.getValues().get(0).getValueString()); + break; + case Complex: throw new FHIRException("SQLite runner does not handle complexes"); + } + } + } catch (Exception e) { + throw new FHIRException(e); + } + + } + + @Override + public void finish(Store store) { + // nothing + } + + @Override + public boolean supportsArrays() { + return false; + } + + @Override + public boolean supportsComplexTypes() { + return false; + } + + @Override + public boolean needsName() { + return true; + } + + @Override + public String getKeyForSourceResource(Base res) { + throw new Error("Key management for resources isn't decided yet"); + } + + @Override + public String getKeyForTargetResource(Base res) { + throw new Error("Key management for resources isn't decided yet"); + } +} diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/sql/Store.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/sql/Store.java new file mode 100644 index 000000000..2a59353ae --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/sql/Store.java @@ -0,0 +1,16 @@ +package org.hl7.fhir.r5.utils.sql; + +public class Store { + + private String name; + + protected Store(String name) { + super(); + this.name = name; + } + + public String getName() { + return name; + } + +} diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/sql/Validator.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/sql/Validator.java new file mode 100644 index 000000000..08ded3af0 --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/sql/Validator.java @@ -0,0 +1,507 @@ +package org.hl7.fhir.r5.utils.sql; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.r5.model.TypeDetails; +import org.hl7.fhir.r5.model.ExpressionNode; +import org.hl7.fhir.r5.model.ExpressionNode.CollectionStatus; +import org.hl7.fhir.r5.utils.FHIRPathEngine; +import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.json.model.JsonArray; +import org.hl7.fhir.utilities.json.model.JsonBoolean; +import org.hl7.fhir.utilities.json.model.JsonElement; +import org.hl7.fhir.utilities.json.model.JsonNumber; +import org.hl7.fhir.utilities.json.model.JsonObject; +import org.hl7.fhir.utilities.json.model.JsonString; +import org.hl7.fhir.utilities.validation.ValidationMessage; +import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; +import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; +import org.hl7.fhir.utilities.validation.ValidationMessage.Source; + +public class Validator { + + private IWorkerContext context; + private FHIRPathEngine fpe; + private List prohibitedNames = new ArrayList(); + private List issues = new ArrayList(); + private boolean arrays; + private boolean complexTypes; + private boolean needsName; + + private String resourceName; + private List columns = new ArrayList(); + private String name; + + protected Validator(IWorkerContext context, FHIRPathEngine fpe, List prohibitedNames, boolean arrays, boolean complexTypes, boolean needsName) { + super(); + this.context = context; + this.fpe = fpe; + this.prohibitedNames = prohibitedNames; + this.arrays = arrays; + this.complexTypes = complexTypes; + this.needsName = needsName; + } + + public String getResourceName() { + return resourceName; + } + + public List getColumns() { + return columns; + } + + public void checkViewDefinition(String path, JsonObject viewDefinition) { + JsonElement nameJ = viewDefinition.get("name"); + if (nameJ == null) { + if (needsName) { + error(path, viewDefinition, "No name provided", IssueType.REQUIRED); + } + } else if (!(nameJ instanceof JsonString)) { + error(path, viewDefinition, "name must be a string", IssueType.INVALID); + } else { + name = nameJ.asString(); + if (!isValidName(name)) { + error(path+".name", nameJ, "The name '"+name+"' is not valid", IssueType.INVARIANT); + } + if (prohibitedNames.contains(name)) { + error(path, nameJ, "The name '"+name+"' on the viewDefinition is not allowed in this context", IssueType.BUSINESSRULE); + } + } + + JsonElement resourceNameJ = viewDefinition.get("resource"); + if (resourceNameJ == null) { + error(path, viewDefinition, "No resource provided", IssueType.REQUIRED); + } else if (!(resourceNameJ instanceof JsonString)) { + error(path, viewDefinition, "resource must be a string", IssueType.INVALID); + } else { + resourceName = resourceNameJ.asString(); + if (!context.getResourceNamesAsSet().contains(resourceName)) { + error(path+".name", nameJ, "The name '"+resourceName+"' is not a valid resource", IssueType.BUSINESSRULE); + } else { + int i = 0; + if (checkAllObjects(path, viewDefinition, "constant")) { + for (JsonObject constant : viewDefinition.getJsonObjects("constant")) { + checkConstant(path+".constant["+i+"]", constant); + i++; + } + } + i = 0; + if (checkAllObjects(path, viewDefinition, "where")) { + for (JsonObject where : viewDefinition.getJsonObjects("where")) { + checkWhere(path+".where["+i+"]", where); + i++; + } + } + TypeDetails t = new TypeDetails(CollectionStatus.SINGLETON, resourceName); + + if (viewDefinition.has("forEach")) { + checkForEach(path, viewDefinition, viewDefinition.get("forEach"), t); + } else if (viewDefinition.has("forEachOrNull")) { + checkForEachOrNull(path, viewDefinition, viewDefinition.get("forEachOrNull"), t); + } else if (viewDefinition.has("union")) { + checkUnion(path, viewDefinition, viewDefinition.get("union"), t); + } else { + i = 0; + if (checkAllObjects(path, viewDefinition, "select")) { + for (JsonObject select : viewDefinition.getJsonObjects("select")) { + checkSelect(path+".select["+i+"]", select, t); + i++; + } + if (i == 0) { + error(path, viewDefinition, "No select statements found", IssueType.REQUIRED); + } + } + } + } + } + } + + private void checkSelect(String path, JsonObject select, TypeDetails t) { + if (select.has("path")) { + checkSelectPath(path, select, select.get("path"), t); + } else if (select.has("forEach")) { + checkForEach(path, select, select.get("forEach"), t); + } else if (select.has("forEachOrNull")) { + checkForEachOrNull(path, select, select.get("forEachOrNull"), t); + } else if (select.has("union")) { + checkUnion(path, select, select.get("union"), t); + } else { + error(path, select, "The select has neither a path, forEach, forEachOrNull, or union statement", IssueType.REQUIRED); + } + } + + private void checkSelectPath(String path, JsonObject select, JsonElement expression, TypeDetails t) { + if (!(expression instanceof JsonString)) { + error(path+".forEach", expression, "forEach is not a string", IssueType.INVALID); + } else { + String expr = expression.asString(); + + List warnings = new ArrayList<>(); + TypeDetails td = null; + ExpressionNode node = null; + try { + node = fpe.parse(expr); + select.setUserData("path", node); + td = fpe.checkOnTypes(null, resourceName, t, node, warnings); + } catch (Exception e) { + error(path, expression, e.getMessage(), IssueType.INVALID); + } + if (td != null && node != null) { + for (String s : warnings) { + warning(path+".path", expression, s); + } + String columnName = null; + JsonElement aliasJ = select.get("alias"); + if (aliasJ != null) { + if (aliasJ instanceof JsonString) { + columnName = aliasJ.asString(); + if (!isValidName(columnName)) { + error(path+".name", aliasJ, "The name '"+columnName+"' is not valid", IssueType.VALUE); + } + } else { + error(path+".alias", aliasJ, "alias must be a string", IssueType.INVALID); + } + } + if (columnName == null) { + List names = node.getDistalNames(); + if (names.size() == 1 && names.get(0) != null) { + columnName = names.get(0); + if (!isValidName(columnName)) { + error(path+".path", expression, "The name '"+columnName+"' found in the path expression is not a valid column name, so an alias is required", IssueType.INVARIANT); + } + } else { + error(path, select, "The path does not resolve to a name, so an alias is required", IssueType.REQUIRED); + } + } + // ok, name is sorted! + if (columnName != null) { + select.setUserData("name", columnName); + boolean isColl = (td.getCollectionStatus() != CollectionStatus.SINGLETON) || column(columnName) != null; + if (select.has("collection")) { + JsonElement collectionJ = select.get("collection"); + if (!(collectionJ instanceof JsonBoolean)) { + error(path+".collection", collectionJ, "collection is not a boolean", IssueType.INVALID); + } else { + boolean collection = collectionJ.asJsonBoolean().asBoolean(); + if (!collection && isColl) { + isColl = false; + warning(path, select, "collection is false, but the path statement(s) might return multiple values for the column '"+columnName+"' some inputs"); + } + } + } + if (isColl && !arrays) { + warning(path, expression, "column appears to be a collection, but this is not allowed in this context"); + } + // ok collection is sorted + Set types = new HashSet<>(); + for (String type : td.getTypes()) { + types.add(simpleType(type)); + } + + JsonElement typeJ = select.get("type"); + if (typeJ != null) { + if (typeJ instanceof JsonString) { + String type = typeJ.asString(); + if (!td.hasType(type)) { + error(path+".type", typeJ, "The path expression does not return a value of the type '"+type, IssueType.VALUE); + } else { + types.clear(); + types.add(simpleType(type)); + } + } else { + error(path+".type", typeJ, "type must be a string", IssueType.INVALID); + } + } + if (types.size() != 1) { + error(path, select, "Unable to determine a type (found "+td.describe()+")", IssueType.BUSINESSRULE); + } else { + String type = types.iterator().next(); + if (!isSimpleType(type) && !complexTypes) { + error(path, expression, "column is a complex type but this is not allowed in this context", IssueType.BUSINESSRULE); + } else { + Column col = column(columnName); + if (col != null) { + if (!col.getType().equals(type)) { + error(path, expression, "Duplicate definition for "+columnName+" has different types ("+col.getType()+" vs "+type+")", IssueType.BUSINESSRULE); + } + if (col.isColl() != isColl) { + error(path, expression, "Duplicate definition for "+columnName+" has different status for collection ("+col.isColl()+" vs "+isColl+")", IssueType.BUSINESSRULE); + } + } else { + columns.add(new Column(columnName, isColl, type, kindForType(type))); + + } + } + } + } + } + } + } + + private ColumnKind kindForType(String type) { + switch (type) { + case "dateTime": return ColumnKind.DateTime; + case "boolean": return ColumnKind.Boolean; + case "integer": return ColumnKind.Integer; + case "decimal": return ColumnKind.Decimal; + case "string": return ColumnKind.String; + case "base64Binary": return ColumnKind.Binary; + case "time": return ColumnKind.Time; + default: return ColumnKind.Complex; + } + } + + private Column column(String columnName) { + for (Column t : columns) { + if (t.getName().equalsIgnoreCase(columnName)) { + return t; + } + } + return null; + } + + private boolean isSimpleType(String type) { + return Utilities.existsInList(type, "dateTime", "boolean", "integer", "decimal", "string", "base64Binary"); + } + + private String simpleType(String type) { + type = type.replace("http://hl7.org/fhirpath/System.", "").replace("http://hl7.org/fhir/StructureDefinition/", ""); + if (Utilities.existsInList(type, "date", "dateTime", "instant")) { + return "dateTime"; + } + if (Utilities.existsInList(type, "Boolean", "boolean")) { + return "boolean"; + } + if (Utilities.existsInList(type, "Integer", "integer", "integer64")) { + return "integer"; + } + if (Utilities.existsInList(type, "Decimal", "decimal")) { + return "decimal"; + } + if (Utilities.existsInList(type, "String", "string", "code")) { + return "string"; + } + if (Utilities.existsInList(type, "Time", "time")) { + return "time"; + } + if (Utilities.existsInList(type, "base64Binary")) { + return "base64Binary"; + } + return type; + } + + private void checkForEach(String path, JsonObject focus, JsonElement expression, TypeDetails t) { + if (!(expression instanceof JsonString)) { + error(path+".forEach", expression, "forEach is not a string", IssueType.INVALID); + } else { + String expr = expression.asString(); + + List warnings = new ArrayList<>(); + TypeDetails td = null; + try { + ExpressionNode n = fpe.parse(expr); + focus.setUserData("forEach", n); + td = fpe.checkOnTypes(null, resourceName, t, n, warnings); + } catch (Exception e) { + error(path, expression, e.getMessage(), IssueType.INVALID); + } + if (td != null) { + for (String s : warnings) { + warning(path+".path", expression, s); + } + int i = 0; + if (checkAllObjects(path, focus, "select")) { + for (JsonObject select : focus.getJsonObjects("select")) { + checkSelect(path+".select["+i+"]", select, td); + i++; + } + if (i == 0) { + error(path, focus, "No select statements found", IssueType.REQUIRED); + } + } + } + } + } + + private void checkForEachOrNull(String path, JsonObject focus, JsonElement expression, TypeDetails t) { + error(path+".forEachOrNull", expression, "forEachOrNull is not supported", IssueType.BUSINESSRULE); + } + + private void checkUnion(String path, JsonObject focus, JsonElement expression, TypeDetails t) { + error(path+".union", focus.get("union"), "union is not supported", IssueType.BUSINESSRULE); + } + + private void checkConstant(String path, JsonObject constant) { + JsonElement nameJ = constant.get("name"); + if (nameJ == null) { + error(path, constant, "No name provided", IssueType.REQUIRED); + } else if (!(nameJ instanceof JsonString)) { + error(path, constant, "Name must be a string", IssueType.INVALID); + } else { + String name = constant.asString("name"); + if (!isValidName(name)) { + error(path+".name", nameJ, "The name '"+name+"' is not valid", IssueType.INVARIANT); + } + } + if (constant.has("valueBase64Binary")) { + checkIsString(path, constant, "valueBase64Binary"); + } else if (constant.has("valueBoolean")) { + checkIsBoolean(path, constant, "valueBoolean"); + } else if (constant.has("valueCanonical")) { + checkIsString(path, constant, "valueCanonical"); + } else if (constant.has("valueCode")) { + checkIsString(path, constant, "valueCode"); + } else if (constant.has("valueDate")) { + checkIsString(path, constant, "valueDate"); + } else if (constant.has("valueDateTime")) { + checkIsString(path, constant, "valueDateTime"); + } else if (constant.has("valueDecimal")) { + checkIsNumber(path, constant, "valueDecimal"); + } else if (constant.has("valueId")) { + checkIsString(path, constant, "valueId"); + } else if (constant.has("valueInstant")) { + checkIsString(path, constant, "valueInstant"); + } else if (constant.has("valueInteger")) { + checkIsNumber(path, constant, "valueInteger"); + } else if (constant.has("valueInteger64")) { + checkIsNumber(path, constant, "valueInteger64"); + } else if (constant.has("valueOid")) { + checkIsString(path, constant, "valueOid"); + } else if (constant.has("valueString")) { + checkIsString(path, constant, "valueString"); + } else if (constant.has("valuePositiveInt")) { + checkIsNumber(path, constant, "valuePositiveInt"); + } else if (constant.has("valueTime")) { + checkIsString(path, constant, "valueTime"); + } else if (constant.has("valueUnsignedInt")) { + checkIsNumber(path, constant, "valueUnsignedInt"); + } else if (constant.has("valueUri")) { + checkIsString(path, constant, "valueUri"); + } else if (constant.has("valueUrl")) { + checkIsString(path, constant, "valueUrl"); + } else if (constant.has("valueUuid")) { + checkIsString(path, constant, "valueUuid"); + } else { + error(path, constant, "No value found", IssueType.REQUIRED); + } + } + + private void checkIsString(String path, JsonObject constant, String name) { + JsonElement j = constant.get(name); + if (!(j instanceof JsonString)) { + error(path+"."+name, j, name+" must be a string", IssueType.INVALID); + } + } + + private void checkIsBoolean(String path, JsonObject constant, String name) { + JsonElement j = constant.get(name); + if (!(j instanceof JsonBoolean)) { + error(path+"."+name, j, name+" must be a boolean", IssueType.INVALID); + } + } + + private void checkIsNumber(String path, JsonObject constant, String name) { + JsonElement j = constant.get(name); + if (!(j instanceof JsonNumber)) { + error(path+"."+name, j, name+" must be a number", IssueType.INVALID); + } + } + private void checkWhere(String path, JsonObject where) { + String expr = where.asString("path"); + if (expr == null) { + error(path, where, "No path provided", IssueType.REQUIRED); + } + List types = new ArrayList<>(); + List warnings = new ArrayList<>(); + types.add(resourceName); + TypeDetails td = null; + try { + ExpressionNode n = fpe.parse(expr); + where.setUserData("path", n); + td = fpe.checkOnTypes(null, resourceName, types, n, warnings); + } catch (Exception e) { + error(path, where.get("path"), e.getMessage(), IssueType.INVALID); + } + if (td != null) { + if (td.getCollectionStatus() != CollectionStatus.SINGLETON || td.getTypes().size() != 1 || !td.hasType("boolean")) { + error(path+".path", where.get("path"), "A where path must return a boolean, but the expression "+expr+" returns a "+td.describe(), IssueType.BUSINESSRULE); + } else { + for (String s : warnings) { + warning(path+".path", where.get("path"), s); + } + } + } + } + + private boolean isValidName(String name) { + boolean first = true; + for (char c : name.toCharArray()) { + if (!(Character.isAlphabetic(c) || Character.isDigit(c) || (!first && c == '_'))) { + return false; + } + first = false; + } + return true; + } + + + private boolean checkAllObjects(String path, JsonObject focus, String name) { + if (!focus.has(name)) { + return true; + } else if (!(focus.get(name) instanceof JsonArray)) { + error(path+"."+name, focus.get(name), name+" must be an array", IssueType.INVALID); + return false; + } else { + JsonArray arr = focus.getJsonArray(name); + int i = 0; + boolean ok = true; + for (JsonElement e : arr) { + if (!(e instanceof JsonObject)) { + error(path+"."+name+"["+i+"]", e, name+"["+i+"] must be an object", IssueType.INVALID); + ok = false; + } + } + return ok; + } + } + + private void error(String path, JsonElement e, String issue, IssueType type) { + ValidationMessage vm = new ValidationMessage(Source.InstanceValidator, type, e.getStart().getLine(), e.getStart().getCol(), path, issue, IssueSeverity.ERROR); + issues.add(vm); + + } + + private void warning(String path, JsonElement e, String issue) { + ValidationMessage vm = new ValidationMessage(Source.InstanceValidator, IssueType.BUSINESSRULE, e.getStart().getLine(), e.getStart().getCol(), path, issue, IssueSeverity.WARNING); + issues.add(vm); + } + + public void dump() { + for (ValidationMessage vm : issues) { + System.out.println(vm.summary()); + } + + } + + public void check() { + boolean ok = true; + for (ValidationMessage vm : issues) { + if (vm.isError()) { + ok = false; + } + } + if (!ok) { + throw new FHIRException("View Definition is not valid"); + } + + } + + public String getName() { + return name; + } +} diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/sql/Value.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/sql/Value.java new file mode 100644 index 000000000..6cc1e2d96 --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/sql/Value.java @@ -0,0 +1,129 @@ +package org.hl7.fhir.r5.utils.sql; + +import java.math.BigDecimal; +import java.util.Date; + +import org.hl7.fhir.r5.model.Base; + + +/** + * String value is always provided, and a more specific value may also be provided + */ + +public class Value { + + private String valueString; + private Boolean valueBoolean; + private Date valueDate; + private Integer valueInt; + private BigDecimal valueDecimal; + private byte[] valueBinary; + private Base valueComplex; + + private Value() { + super(); + } + + public static Value makeString(String s) { + Value v = new Value(); + v.valueString = s; + return v; + } + + public static Value makeBoolean(String s, Boolean b) { + Value v = new Value(); + v.valueString = s; + v.valueBoolean = b; + return v; + } + + public static Value makeDate(String s, Date d) { + Value v = new Value(); + v.valueString = s; + v.valueDate = d; + return v; + } + + public static Value makeInteger(String s, Integer i) { + Value v = new Value(); + v.valueString = s; + v.valueInt = i; + return v; + } + + + public static Value makeDecimal(String s, BigDecimal bigDecimal) { + Value v = new Value(); + v.valueString = s; + v.valueDecimal = bigDecimal; + return v; + } + + public static Value makeBinary(String s, byte[] b) { + Value v = new Value(); + v.valueString = s; + v.valueBinary = b; + return v; + } + + public static Value makeComplex(Base b) { + Value v = new Value(); + v.valueComplex = b; + return v; + } + public String getValueString() { + return valueString; + } + + public Date getValueDate() { + return valueDate; + } + + public Integer getValueInt() { + return valueInt; + } + + public BigDecimal getValueDecimal() { + return valueDecimal; + } + + public byte[] getValueBinary() { + return valueBinary; + } + + public Boolean getValueBoolean() { + return valueBoolean; + } + + public Base getValueComplex() { + return valueComplex; + } + + public boolean hasValueString() { + return valueString != null; + } + + public boolean hasValueDate() { + return valueDate != null; + } + + public boolean hasValueInt() { + return valueInt != null; + } + + public boolean hasValueDecimal() { + return valueDecimal != null; + } + + public boolean hasValueBinary() { + return valueBinary != null; + } + + public boolean hasValueBoolean() { + return valueBoolean != null; + } + + public boolean hasValueComplex() { + return valueComplex != null; + } +} diff --git a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/sql/SQLOnFhirTestCases.java b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/sql/SQLOnFhirTestCases.java new file mode 100644 index 000000000..5b9e36adb --- /dev/null +++ b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/sql/SQLOnFhirTestCases.java @@ -0,0 +1,169 @@ +package org.hl7.fhir.r5.sql; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Stream; + +import javax.xml.parsers.ParserConfigurationException; + +import org.fhir.ucum.UcumException; +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.r5.model.Base; +import org.hl7.fhir.r5.model.Factory; +import org.hl7.fhir.r5.model.Resource; +import org.hl7.fhir.r5.model.ResourceFactory; +import org.hl7.fhir.r5.sql.SQLOnFhirTestCases.RowSorter; +import org.hl7.fhir.r5.test.utils.CompareUtilities; +import org.hl7.fhir.r5.test.utils.TestingUtilities; +import org.hl7.fhir.r5.utils.sql.Provider; +import org.hl7.fhir.r5.utils.sql.Runner; +import org.hl7.fhir.r5.utils.sql.StorageJson; +import org.hl7.fhir.utilities.json.model.JsonArray; +import org.hl7.fhir.utilities.json.model.JsonElement; +import org.hl7.fhir.utilities.json.model.JsonObject; +import org.hl7.fhir.utilities.json.parser.JsonParser; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.xml.sax.SAXException; + +public class SQLOnFhirTestCases { + + public class TestProvider implements Provider { + + @Override + public List fetch(String resourceType) { + List list = new ArrayList(); + for (JsonObject res : details.resources) { + try { + String src = JsonParser.compose(res, false); + Resource resource = new org.hl7.fhir.r5.formats.JsonParser().parse(src); + if (resource.fhirType().equals(resourceType)) { + list.add(resource); + } + } catch (Exception e) { + throw new FHIRException(e); + } + } + return list; + } + + @Override + public Base resolveReference(String ref, String rt) { + if (ref == null) { + return null; + } + String[] p = ref.split("\\/"); + if (p.length > 1 && TestingUtilities.getSharedWorkerContext().getResourceNamesAsSet().contains(p[p.length-2])) { + if (rt == null || rt.equals(p[p.length-2])) { + return ResourceFactory.createResource(p[p.length-2]).setId(p[p.length-1]); + } + } + + return null; + } + + } + + private static class TestDetails { + String name; + String path; + List resources; + JsonObject testCase; + protected TestDetails(String name, String path, List resources, JsonObject testCase) { + super(); + this.name = name; + this.path = path; + this.resources = resources; + this.testCase = testCase; + } + + } + + public static Stream data() throws ParserConfigurationException, SAXException, IOException { + List objects = new ArrayList<>(); + File dir = new File("/Users/grahamegrieve/work/sql-on-fhir-v2/tests/content"); + for (File f : dir.listFiles()) { + if (f.getName().endsWith(".json")) { + JsonObject json = JsonParser.parseObject(f); + String name1 = f.getName().replace(".json", ""); + List resources = json.getJsonObjects("resources"); + int i = 0; + for (JsonObject test : json.getJsonObjects("tests")) { + String name2 = test.asString("title"); + objects.add(Arguments.of(name1+":"+name2, new TestDetails(name1+":"+name2, "$.tests["+i+"]", resources, test))); + i++; + } + } + } + return objects.stream(); + } + + private TestDetails details; + + @SuppressWarnings("deprecation") + @ParameterizedTest(name = "{index}: file {0}") + @MethodSource("data") + public void test(String name, TestDetails test) throws FileNotFoundException, IOException, FHIRException, org.hl7.fhir.exceptions.FHIRException, UcumException { + this.details = test; + Runner runner = new Runner(); + runner.setContext(TestingUtilities.getSharedWorkerContext()); + runner.setProvider(new TestProvider()); + StorageJson store = new StorageJson(); + runner.setStorage(store); + + System.out.println(""); + System.out.println("----------------------------------------------------------------------------"); + System.out.println(test.name); + JsonArray results = null; + try { + runner.execute(test.path+".view", test.testCase.getJsonObject("view")); + results = store.getRows(); + } catch (Exception e) { + Assertions.assertTrue(test.testCase.has("expectError"), e.getMessage()); + } + if (results != null) { + System.out.println(JsonParser.compose(results, true)); + if (test.testCase.has("expect")) { + JsonObject rows = new JsonObject(); + rows.add("rows", results); + JsonObject exp = new JsonObject(); + exp.add("rows", test.testCase.getJsonArray("expect")); + sortResults(exp); + sortResults(rows); + String expS = JsonParser.compose(exp, true); + String rowS = JsonParser.compose(rows, true); + String c = CompareUtilities.checkJsonSrcIsSame(expS, rowS, null); + Assertions.assertNull(c, c); + } else if (test.testCase.has("expectCount")) { + Assertions.assertEquals(test.testCase.asInteger("expectCount"), results.size()); + } else { + Assertions.fail("?"); + } + } + } + + + public class RowSorter implements Comparator { + + @Override + public int compare(JsonElement e1, JsonElement e2) { + String s1 = JsonParser.compose(e1); + String s2 = JsonParser.compose(e2); + return s1.compareTo(s2); + } + + } + + private void sortResults(JsonObject o) { + Collections.sort(o.getJsonArray("rows").getItems(), new RowSorter()); + + } + +} diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/CommaSeparatedStringBuilder.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/CommaSeparatedStringBuilder.java index 757292b47..7cdc295b7 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/CommaSeparatedStringBuilder.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/CommaSeparatedStringBuilder.java @@ -117,7 +117,9 @@ public class CommaSeparatedStringBuilder { public static String join(String sep, Collection list) { CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(sep); for (String s : list) { - b.append(s); + if (s != null) { + b.append(s); + } } return b.toString(); }