001package org.hl7.fhir.r4.utils;
002
003import java.util.ArrayList;
004import java.util.Arrays;
005import java.util.HashMap;
006import java.util.List;
007import java.util.Map;
008
009/*
010  Copyright (c) 2011+, HL7, Inc.
011  All rights reserved.
012  
013  Redistribution and use in source and binary forms, with or without modification, 
014  are permitted provided that the following conditions are met:
015    
016   * Redistributions of source code must retain the above copyright notice, this 
017     list of conditions and the following disclaimer.
018   * Redistributions in binary form must reproduce the above copyright notice, 
019     this list of conditions and the following disclaimer in the documentation 
020     and/or other materials provided with the distribution.
021   * Neither the name of HL7 nor the names of its contributors may be used to 
022     endorse or promote products derived from this software without specific 
023     prior written permission.
024  
025  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
026  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
027  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
028  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
029  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
030  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
031  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
032  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
033  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
034  POSSIBILITY OF SUCH DAMAGE.
035  
036 */
037
038import org.hl7.fhir.exceptions.FHIRException;
039import org.hl7.fhir.exceptions.PathEngineException;
040import org.hl7.fhir.r4.context.IWorkerContext;
041import org.hl7.fhir.r4.fhirpath.ExpressionNode;
042import org.hl7.fhir.r4.fhirpath.FHIRPathEngine;
043import org.hl7.fhir.r4.fhirpath.TypeDetails;
044import org.hl7.fhir.r4.fhirpath.FHIRPathEngine.ExpressionNodeWithOffset;
045import org.hl7.fhir.r4.fhirpath.FHIRPathEngine.IEvaluationContext;
046import org.hl7.fhir.r4.fhirpath.FHIRPathUtilityClasses.FunctionDetails;
047import org.hl7.fhir.r4.model.Base;
048import org.hl7.fhir.r4.model.Resource;
049import org.hl7.fhir.r4.model.Tuple;
050import org.hl7.fhir.r4.model.ValueSet;
051import org.hl7.fhir.utilities.Utilities;
052
053public class LiquidEngine implements IEvaluationContext {
054
055  public interface ILiquidEngineIcludeResolver {
056    public String fetchInclude(LiquidEngine engine, String name);
057  }
058
059  private IEvaluationContext externalHostServices;
060  private FHIRPathEngine engine;
061  private ILiquidEngineIcludeResolver includeResolver;
062
063  private class LiquidEngineContext {
064    private Object externalContext;
065    private Map<String, Base> vars = new HashMap<>();
066
067    public LiquidEngineContext(Object externalContext) {
068      super();
069      this.externalContext = externalContext;
070    }
071
072    public LiquidEngineContext(LiquidEngineContext existing) {
073      super();
074      externalContext = existing.externalContext;
075      vars.putAll(existing.vars);
076    }
077  }
078
079  public LiquidEngine(IWorkerContext context, IEvaluationContext hostServices) {
080    super();
081    this.externalHostServices = hostServices;
082    engine = new FHIRPathEngine(context);
083    engine.setHostServices(this);
084  }
085
086  public ILiquidEngineIcludeResolver getIncludeResolver() {
087    return includeResolver;
088  }
089
090  public void setIncludeResolver(ILiquidEngineIcludeResolver includeResolver) {
091    this.includeResolver = includeResolver;
092  }
093
094  public LiquidDocument parse(String source, String sourceName) throws FHIRException {
095    return new LiquidParser(source).parse(sourceName);
096  }
097
098  public String evaluate(LiquidDocument document, Resource resource, Object appContext) throws FHIRException {
099    StringBuilder b = new StringBuilder();
100    LiquidEngineContext ctxt = new LiquidEngineContext(appContext);
101    for (LiquidNode n : document.body) {
102      n.evaluate(b, resource, ctxt);
103    }
104    return b.toString();
105  }
106
107  private abstract class LiquidNode {
108    protected void closeUp() {
109    }
110
111    public abstract void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException;
112  }
113
114  private class LiquidConstant extends LiquidNode {
115    private String constant;
116    private StringBuilder b = new StringBuilder();
117
118    @Override
119    protected void closeUp() {
120      constant = b.toString();
121      b = null;
122    }
123
124    public void addChar(char ch) {
125      b.append(ch);
126    }
127
128    @Override
129    public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) {
130      b.append(constant);
131    }
132  }
133
134  private class LiquidStatement extends LiquidNode {
135    private String statement;
136    private ExpressionNode compiled;
137
138    @Override
139    public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException {
140      if (compiled == null)
141        compiled = engine.parse(statement);
142      b.append(engine.evaluateToString(ctxt, resource, resource, resource, compiled));
143    }
144  }
145
146  private class LiquidIf extends LiquidNode {
147    private String condition;
148    private ExpressionNode compiled;
149    private List<LiquidNode> thenBody = new ArrayList<>();
150    private List<LiquidNode> elseBody = new ArrayList<>();
151
152    @Override
153    public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException {
154      if (compiled == null)
155        compiled = engine.parse(condition);
156      boolean ok = engine.evaluateToBoolean(ctxt, resource, resource, resource, compiled);
157      List<LiquidNode> list = ok ? thenBody : elseBody;
158      for (LiquidNode n : list) {
159        n.evaluate(b, resource, ctxt);
160      }
161    }
162  }
163
164  private class LiquidLoop extends LiquidNode {
165    private String varName;
166    private String condition;
167    private ExpressionNode compiled;
168    private List<LiquidNode> body = new ArrayList<>();
169
170    @Override
171    public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException {
172      if (compiled == null)
173        compiled = engine.parse(condition);
174      List<Base> list = engine.evaluate(ctxt, resource, resource, resource, compiled);
175      LiquidEngineContext lctxt = new LiquidEngineContext(ctxt);
176      for (Base o : list) {
177        lctxt.vars.put(varName, o);
178        for (LiquidNode n : body) {
179          n.evaluate(b, resource, lctxt);
180        }
181      }
182    }
183  }
184
185  private class LiquidInclude extends LiquidNode {
186    private String page;
187    private Map<String, ExpressionNode> params = new HashMap<>();
188
189    @Override
190    public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException {
191      String src = includeResolver.fetchInclude(LiquidEngine.this, page);
192      LiquidParser parser = new LiquidParser(src);
193      LiquidDocument doc = parser.parse(page);
194      LiquidEngineContext nctxt = new LiquidEngineContext(ctxt.externalContext);
195      Tuple incl = new Tuple();
196      nctxt.vars.put("include", incl);
197      for (String s : params.keySet()) {
198        incl.addProperty(s, engine.evaluate(ctxt, resource, resource, resource, params.get(s)));
199      }
200      for (LiquidNode n : doc.body) {
201        n.evaluate(b, resource, nctxt);
202      }
203    }
204  }
205
206  public static class LiquidDocument {
207    private List<LiquidNode> body = new ArrayList<>();
208
209  }
210
211  private class LiquidParser {
212
213    private String source;
214    private int cursor;
215    private String name;
216
217    public LiquidParser(String source) {
218      this.source = source;
219      cursor = 0;
220    }
221
222    private char next1() {
223      if (cursor >= source.length())
224        return 0;
225      else
226        return source.charAt(cursor);
227    }
228
229    private char next2() {
230      if (cursor >= source.length() - 1)
231        return 0;
232      else
233        return source.charAt(cursor + 1);
234    }
235
236    private char grab() {
237      cursor++;
238      return source.charAt(cursor - 1);
239    }
240
241    public LiquidDocument parse(String name) throws FHIRException {
242      this.name = name;
243      LiquidDocument doc = new LiquidDocument();
244      parseList(doc.body, new String[0]);
245      return doc;
246    }
247
248    private String parseList(List<LiquidNode> list, String[] terminators) throws FHIRException {
249      String close = null;
250      while (cursor < source.length()) {
251        if (next1() == '{' && (next2() == '%' || next2() == '{')) {
252          if (next2() == '%') {
253            String cnt = parseTag('%');
254            if (Utilities.existsInList(cnt, terminators)) {
255              close = cnt;
256              break;
257            } else if (cnt.startsWith("if "))
258              list.add(parseIf(cnt));
259            else if (cnt.startsWith("loop "))
260              list.add(parseLoop(cnt.substring(4).trim()));
261            else if (cnt.startsWith("include "))
262              list.add(parseInclude(cnt.substring(7).trim()));
263            else
264              throw new FHIRException(
265                  "Script " + name + ": Script " + name + ": Unknown flow control statement " + cnt);
266          } else { // next2() == '{'
267            list.add(parseStatement());
268          }
269        } else {
270          if (list.size() == 0 || !(list.get(list.size() - 1) instanceof LiquidConstant))
271            list.add(new LiquidConstant());
272          ((LiquidConstant) list.get(list.size() - 1)).addChar(grab());
273        }
274      }
275      for (LiquidNode n : list)
276        n.closeUp();
277      if (terminators.length > 0)
278        if (!Utilities.existsInList(close, terminators))
279          throw new FHIRException(
280              "Script " + name + ": Script " + name + ": Found end of script looking for " + terminators);
281      return close;
282    }
283
284    private LiquidNode parseIf(String cnt) throws FHIRException {
285      LiquidIf res = new LiquidIf();
286      res.condition = cnt.substring(3).trim();
287      String term = parseList(res.thenBody, new String[] { "else", "endif" });
288      if ("else".equals(term))
289        term = parseList(res.elseBody, new String[] { "endif" });
290      return res;
291    }
292
293    private LiquidNode parseInclude(String cnt) throws FHIRException {
294      int i = 1;
295      while (i < cnt.length() && !Character.isWhitespace(cnt.charAt(i)))
296        i++;
297      if (i == cnt.length() || i == 0)
298        throw new FHIRException("Script " + name + ": Error reading include: " + cnt);
299      LiquidInclude res = new LiquidInclude();
300      res.page = cnt.substring(0, i);
301      while (i < cnt.length() && Character.isWhitespace(cnt.charAt(i)))
302        i++;
303      while (i < cnt.length()) {
304        int j = i;
305        while (i < cnt.length() && cnt.charAt(i) != '=')
306          i++;
307        if (i >= cnt.length() || j == i)
308          throw new FHIRException("Script " + name + ": Error reading include: " + cnt);
309        String n = cnt.substring(j, i);
310        if (res.params.containsKey(n))
311          throw new FHIRException("Script " + name + ": Error reading include: " + cnt);
312        i++;
313        ExpressionNodeWithOffset t = engine.parsePartial(cnt, i);
314        i = t.getOffset();
315        res.params.put(n, t.getNode());
316        while (i < cnt.length() && Character.isWhitespace(cnt.charAt(i)))
317          i++;
318      }
319      return res;
320    }
321
322    private LiquidNode parseLoop(String cnt) throws FHIRException {
323      int i = 0;
324      while (!Character.isWhitespace(cnt.charAt(i)))
325        i++;
326      LiquidLoop res = new LiquidLoop();
327      res.varName = cnt.substring(0, i);
328      while (Character.isWhitespace(cnt.charAt(i)))
329        i++;
330      int j = i;
331      while (!Character.isWhitespace(cnt.charAt(i)))
332        i++;
333      if (!"in".equals(cnt.substring(j, i)))
334        throw new FHIRException("Script " + name + ": Script " + name + ": Error reading loop: " + cnt);
335      res.condition = cnt.substring(i).trim();
336      parseList(res.body, new String[] { "endloop" });
337      return res;
338    }
339
340    private String parseTag(char ch) throws FHIRException {
341      grab();
342      grab();
343      StringBuilder b = new StringBuilder();
344      while (cursor < source.length() && !(next1() == '%' && next2() == '}')) {
345        b.append(grab());
346      }
347      if (!(next1() == '%' && next2() == '}'))
348        throw new FHIRException("Script " + name + ": Unterminated Liquid statement {% " + b.toString());
349      grab();
350      grab();
351      return b.toString().trim();
352    }
353
354    private LiquidStatement parseStatement() throws FHIRException {
355      grab();
356      grab();
357      StringBuilder b = new StringBuilder();
358      while (cursor < source.length() && !(next1() == '}' && next2() == '}')) {
359        b.append(grab());
360      }
361      if (!(next1() == '}' && next2() == '}'))
362        throw new FHIRException("Script " + name + ": Unterminated Liquid statement {{ " + b.toString());
363      grab();
364      grab();
365      LiquidStatement res = new LiquidStatement();
366      res.statement = b.toString().trim();
367      return res;
368    }
369
370  }
371
372  @Override
373  public List<Base> resolveConstant(FHIRPathEngine engine, Object appContext, String name, boolean beforeContext, boolean explicitConstant) throws PathEngineException {
374    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
375    if (ctxt.vars.containsKey(name))
376      return new ArrayList<>(Arrays.asList(ctxt.vars.get(name)));
377    if (externalHostServices == null)
378      return null;
379    return externalHostServices.resolveConstant(engine, ctxt.externalContext, name, beforeContext, explicitConstant);
380  }
381
382  @Override
383  public TypeDetails resolveConstantType(FHIRPathEngine engine, Object appContext, String name, boolean explicitConstant) throws PathEngineException {
384    if (externalHostServices == null)
385      return null;
386    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
387    return externalHostServices.resolveConstantType(engine, ctxt.externalContext, name, explicitConstant);
388  }
389
390  @Override
391  public boolean log(String argument, List<Base> focus) {
392    if (externalHostServices == null)
393      return false;
394    return externalHostServices.log(argument, focus);
395  }
396
397  @Override
398  public FunctionDetails resolveFunction(FHIRPathEngine engine, String functionName) {
399    if (externalHostServices == null)
400      return null;
401    return externalHostServices.resolveFunction(engine, functionName);
402  }
403
404  @Override
405  public TypeDetails checkFunction(FHIRPathEngine engine, Object appContext, String functionName, TypeDetails focus, List<TypeDetails> parameters)
406      throws PathEngineException {
407    if (externalHostServices == null)
408      return null;
409    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
410    return externalHostServices.checkFunction(engine, ctxt.externalContext, functionName, focus, parameters);
411  }
412
413  @Override
414  public List<Base> executeFunction(FHIRPathEngine engine, Object appContext, List<Base> focus, String functionName,
415      List<List<Base>> parameters) {
416    if (externalHostServices == null)
417      return null;
418    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
419    return externalHostServices.executeFunction(engine, ctxt.externalContext, focus, functionName, parameters);
420  }
421
422  @Override
423  public Base resolveReference(FHIRPathEngine engine, Object appContext, String url, Base base) throws FHIRException {
424    if (externalHostServices == null)
425      return null;
426    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
427    return resolveReference(engine, ctxt.externalContext, url, base);
428  }
429
430  @Override
431  public boolean conformsToProfile(FHIRPathEngine engine, Object appContext, Base item, String url) throws FHIRException {
432    if (externalHostServices == null)
433      return false;
434    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
435    return conformsToProfile(engine, ctxt.externalContext, item, url);
436  }
437
438  @Override
439  public ValueSet resolveValueSet(FHIRPathEngine engine, Object appContext, String url) {
440    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
441    if (externalHostServices != null)
442      return externalHostServices.resolveValueSet(engine, ctxt.externalContext, url);
443    else
444      return engine.getWorker().fetchResource(ValueSet.class, url);
445  }
446
447}