001package org.hl7.fhir.r5.liquid;
002
003import java.time.LocalDate;
004import java.time.LocalDateTime;
005import java.time.ZonedDateTime;
006import java.time.format.DateTimeFormatter;
007import java.time.format.DateTimeParseException;
008import java.util.ArrayList;
009import java.util.List;
010import java.util.Locale;
011import java.util.Map;
012
013import org.hl7.fhir.exceptions.FHIRException;
014import org.hl7.fhir.r5.fhirpath.ExpressionNode.CollectionStatus;
015import org.hl7.fhir.r5.fhirpath.FHIRPathEngine;
016import org.hl7.fhir.r5.fhirpath.FHIRPathEngine.IEvaluationContext.FunctionDefinition;
017import org.hl7.fhir.r5.fhirpath.FHIRPathUtilityClasses.FunctionDetails;
018import org.hl7.fhir.r5.fhirpath.TypeDetails;
019import org.hl7.fhir.r5.model.Base;
020import org.hl7.fhir.r5.model.StringType;
021import org.hl7.fhir.r5.testfactory.TestDataFactory.DataTable;
022import org.hl7.fhir.r5.testfactory.dataprovider.TableDataProvider;
023import org.hl7.fhir.utilities.FhirPublication;
024import org.hl7.fhir.utilities.MarkedToMoveToAdjunctPackage;
025import org.hl7.fhir.utilities.Utilities;
026
027// this class exists to allow the Liquid Engine to be used against raw JSON
028
029@MarkedToMoveToAdjunctPackage
030public class BaseTableWrapper extends Base {
031
032
033  private static final long serialVersionUID = 1L;
034  private List<String> columns;
035  private List<String> values;
036  private List<List<String>> rows;
037  private String value; 
038  private Map<String, DataTable> tables;
039
040  private BaseTableWrapper() {
041    super();
042  }
043
044  public static BaseTableWrapper forRows(List<String> columns, List<List<String>> rows) {
045    BaseTableWrapper self = new BaseTableWrapper();
046    self.columns = columns;
047    self.rows = rows;
048    return self;
049  }
050
051  public static BaseTableWrapper forRow(List<String> columns, List<String> values) {
052    BaseTableWrapper self = new BaseTableWrapper();
053    self.columns = columns;
054    self.values = values;
055    return self;
056  }
057
058  public static BaseTableWrapper forCell(String value) {
059    BaseTableWrapper self = new BaseTableWrapper();
060    self.value = value;
061    return self;
062  }
063
064  public static BaseTableWrapper forTable(TableDataProvider dt) {
065    BaseTableWrapper self = new BaseTableWrapper();
066    self.columns = dt.columns();
067    self.rows = new ArrayList<>();
068    dt.reset();
069    while (dt.nextRow()) {
070      self.rows.add(dt.cells());
071    }
072    return self;
073  }
074
075  public static BaseTableWrapper forTable(DataTable dt) {
076    BaseTableWrapper self = new BaseTableWrapper();
077    self.columns = dt.getColumns();
078    self.rows = dt.getRows();
079    return self;
080  }
081
082  public Map<String, DataTable> getTables() {
083    return tables;
084  }
085
086  public BaseTableWrapper setTables(Map<String, DataTable> tables) {
087    this.tables = tables;
088    return this;
089  }
090
091  @Override
092  public String fhirType() {
093    if (values != null || rows != null) {
094      return "Object";
095    } else if (Utilities.existsInList(value, "true", "false")) {
096      return "boolean";
097    } else if (Utilities.isInteger(value)) {
098      return "integer";
099    } else if (Utilities.isDecimal(value, true)) {
100      return "decimal";
101    } else if (Utilities.isAbsoluteUrl(value)) {
102      return "url";
103    } else {
104      return "string";
105    }
106  }
107
108  @Override
109  public String getIdBase() {
110    if (columns == null) {
111      return null;
112    }
113
114    int i = columns.indexOf("id");
115    if (i > -1) {
116      return values.get(i);
117    } else {
118      return null;
119    }
120  }
121
122  @Override
123  public void setIdBase(String value) {
124    throw new Error("BaseTableWrapper is read only");
125  }
126
127  @Override
128  public Base copy() {
129    throw new Error("BaseTableWrapper is read only");
130  }
131
132  @Override
133  public FhirPublication getFHIRPublicationVersion() {
134    return FhirPublication.R5;
135  }
136
137  public String cell(String name) {
138    if (values != null) {
139      int i = columns.indexOf(name);
140      if (i > -1) {
141        return values.get(i);
142      }
143    }
144    return null;
145  }
146
147  public Base[] getProperty(int hash, String name, boolean checkValid) throws FHIRException {
148    if (rows != null && "rows".equals(name)) {
149      Base[] l = new Base[rows.size()];
150      for (int i = 0; i < rows.size(); i++) {
151        l[i] = BaseTableWrapper.forRow(columns, rows.get(i));
152      }
153      return l;      
154    }
155    if (values != null) {
156      int i = columns.indexOf(name);
157      if (i > -1) {
158        Base[] l = new Base[1];
159        l[0] = BaseTableWrapper.forCell(values.get(i));
160        return l;
161      }
162    } 
163    if ("row".equals(name) && values != null) {
164      Base[] l = new Base[1];
165      l[0] = this;
166      return l;
167    }
168    if (tables != null && tables.containsKey(name)) {
169      Base[] l = new Base[1];
170      l[0] = BaseTableWrapper.forTable(tables.get(name)).setTables(tables);
171      return l;      
172    }
173    return super.getProperty(hash, name, checkValid);
174  }
175
176  @Override
177  public String toString() {
178    return value;
179  }
180
181
182  @Override  
183  public boolean isPrimitive() {
184    return value != null;
185  }
186
187
188  @Override 
189  public String primitiveValue() {
190    return value;
191  }
192
193  public static class TableColumnFunction extends FunctionDefinition {
194
195    @Override
196    public String name() {
197      return "column";
198    }
199
200    @Override
201    public FunctionDetails details() {
202      return new FunctionDetails("Look up a column by name", 1, 1);
203    }
204
205    @Override
206    public TypeDetails check(FHIRPathEngine engine, Object appContext, TypeDetails focus, List<TypeDetails> parameters) {
207      if (focus.hasType("Table")) {
208        return new TypeDetails(CollectionStatus.SINGLETON, "string");
209      } else {
210        return null;
211      }
212    }
213
214    @Override
215    public List<Base> execute(FHIRPathEngine engine, Object appContext, List<Base> focus, List<List<Base>> parameters) {
216      List<Base> list = new ArrayList<>();
217      if (focus.size() == 1 && focus.get(0) instanceof BaseTableWrapper && parameters.size() == 1 && parameters.get(0).size() == 1) {
218        BaseTableWrapper tbl = (BaseTableWrapper) focus.get(0);
219        String name = parameters.get(0).get(0).primitiveValue();
220        if (tbl.columns.contains(name)) {
221          list.add(new StringType(tbl.cell(name)));
222        }
223      }
224      return list;
225    }
226
227  }
228
229  public static class TableDateColumnFunction extends FunctionDefinition {
230
231    @Override
232    public String name() {
233      return "dateColumn";
234    }
235
236    @Override
237    public FunctionDetails details() {
238      return new FunctionDetails("read a date(/time) column with the specified format", 2, 2);
239    }
240
241    @Override
242    public TypeDetails check(FHIRPathEngine engine, Object appContext, TypeDetails focus, List<TypeDetails> parameters) {
243      if (focus.hasType("Table")) {
244        return new TypeDetails(CollectionStatus.SINGLETON, "string");
245      } else {
246        return null;
247      }
248    }
249
250    @Override
251    public List<Base> execute(FHIRPathEngine engine, Object appContext, List<Base> focus, List<List<Base>> parameters) {
252      List<Base> list = new ArrayList<>();
253      if (focus.size() == 1 && focus.get(0) instanceof BaseTableWrapper && parameters.size() == 2 && parameters.get(0).size() == 1 && parameters.get(1).size() == 1) {
254        BaseTableWrapper tbl = (BaseTableWrapper) focus.get(0);
255        String name = parameters.get(0).get(0).primitiveValue();
256        String format = parameters.get(1).get(0).primitiveValue();
257        if (tbl.columns.contains(name)) {
258          String cell = tbl.cell(name);
259          if (!Utilities.noString(cell)) {
260            if ("excel.date".equals(format)) {
261              list.add(new StringType(cell.substring(0, 10)));              
262            } else {
263              try {
264                list.add(new StringType(convertToFhirDateTime(cell, format)));
265              } catch (DateTimeParseException e) {
266                throw new IllegalArgumentException("Invalid date-time format: " + cell+" for format "+format, e);
267              }
268            }
269          }
270        }
271      }
272      return list;
273    }
274
275    public static String convertToFhirDateTime(String dateTime, String inputFormat) {
276      DateTimeFormatter inputFormatter = DateTimeFormatter.ofPattern(inputFormat, Locale.ENGLISH);
277      DateTimeFormatter outputFormatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME; // For FHIR DateTime with time zone
278
279      if (inputFormat.contains("Z") || inputFormat.contains("x") || inputFormat.contains("X")) {
280        ZonedDateTime parsedDateTime = ZonedDateTime.parse(dateTime, inputFormatter);
281        return parsedDateTime.format(outputFormatter);
282      } else if (inputFormat.contains("h")) {
283        LocalDateTime parsedDateTime = LocalDateTime.parse(dateTime, inputFormatter);
284        return parsedDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); // FHIR DateTime without time zone
285      } else {
286        LocalDate parsedDate = LocalDate.parse(dateTime, inputFormatter);
287        return parsedDate.format(DateTimeFormatter.ISO_LOCAL_DATE); // FHIR DateTime without time zone
288      }
289    }
290  }
291
292}