001package org.hl7.fhir.r4.utils;
002
003import java.util.*;
004
005import org.hl7.fhir.exceptions.FHIRException;
006import org.hl7.fhir.exceptions.PathEngineException;
007import org.hl7.fhir.r4.context.IWorkerContext;
008import org.hl7.fhir.r4.model.Base;
009import org.hl7.fhir.r4.model.Resource;
010import org.hl7.fhir.r4.model.Tuple;
011import org.hl7.fhir.r4.model.ExpressionNode;
012import org.hl7.fhir.r4.model.Resource;
013import org.hl7.fhir.r4.model.TypeDetails;
014import org.hl7.fhir.r4.utils.FHIRPathEngine.ExpressionNodeWithOffset;
015import org.hl7.fhir.r4.utils.FHIRPathEngine.IEvaluationContext;
016import org.hl7.fhir.utilities.Utilities;
017
018public class LiquidEngine implements IEvaluationContext {
019
020  public interface ILiquidEngineIcludeResolver {
021    public String fetchInclude(LiquidEngine engine, String name);
022  }
023  
024  private IEvaluationContext externalHostServices;
025  private FHIRPathEngine engine;
026  private ILiquidEngineIcludeResolver includeResolver; 
027
028  private class LiquidEngineContext {
029    private Object externalContext;
030    private Map<String, Base> vars = new HashMap<>();
031
032    public LiquidEngineContext(Object externalContext) {
033      super();
034      this.externalContext = externalContext;
035    }
036
037    public LiquidEngineContext(LiquidEngineContext existing) {
038      super();
039      externalContext = existing.externalContext;
040      vars.putAll(existing.vars);
041    }
042  }
043
044  public LiquidEngine(IWorkerContext context, IEvaluationContext hostServices) {
045    super();
046    this.externalHostServices = hostServices;
047    engine = new FHIRPathEngine(context);
048    engine.setHostServices(this);
049  }
050  
051  public ILiquidEngineIcludeResolver getIncludeResolver() {
052    return includeResolver;
053  }
054
055  public void setIncludeResolver(ILiquidEngineIcludeResolver includeResolver) {
056    this.includeResolver = includeResolver;
057  }
058
059  public LiquidDocument parse(String source, String sourceName) throws Exception {
060    return new LiquidParser(source).parse(sourceName);
061  }
062
063  public String evaluate(LiquidDocument document, Resource resource, Object appContext) throws FHIRException {
064    StringBuilder b = new StringBuilder();
065    LiquidEngineContext ctxt = new LiquidEngineContext(appContext);
066    for (LiquidNode n : document.body) {
067      n.evaluate(b, resource, ctxt);
068    }
069    return b.toString();
070  }
071
072  private abstract class LiquidNode {
073    protected void closeUp() {}
074
075    public abstract void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException;
076  }
077
078  private class LiquidConstant extends LiquidNode {
079    private String constant;
080    private StringBuilder b = new StringBuilder();
081
082    @Override
083    protected void closeUp() {
084      constant = b.toString();
085      b = null;
086    }
087
088    public void addChar(char ch) {
089      b.append(ch);
090    }
091
092    @Override
093    public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) {
094      b.append(constant);
095    }
096  }
097
098  private class LiquidStatement extends LiquidNode {
099    private String statement;
100    private ExpressionNode compiled;
101
102    @Override
103    public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException {
104      if (compiled == null)
105        compiled = engine.parse(statement);
106      b.append(engine.evaluateToString(ctxt, resource, resource, compiled));
107    }
108  }
109
110  private class LiquidIf extends LiquidNode {
111    private String condition;
112    private ExpressionNode compiled;
113    private List<LiquidNode> thenBody = new ArrayList<>();
114    private List<LiquidNode> elseBody = new ArrayList<>();
115
116    @Override
117    public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException {
118      if (compiled == null)
119        compiled = engine.parse(condition);
120      boolean ok = engine.evaluateToBoolean(ctxt, resource, resource, compiled); 
121      List<LiquidNode> list = ok ? thenBody : elseBody;
122      for (LiquidNode n : list) {
123        n.evaluate(b, resource, ctxt);
124      }
125    }
126  }
127
128  private class LiquidLoop extends LiquidNode {
129    private String varName;
130    private String condition;
131    private ExpressionNode compiled;
132    private List<LiquidNode> body = new ArrayList<>();
133    @Override
134    public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException {
135      if (compiled == null)
136        compiled = engine.parse(condition);
137      List<Base> list = engine.evaluate(ctxt, resource, resource, compiled);
138      LiquidEngineContext lctxt = new LiquidEngineContext(ctxt);
139      for (Base o : list) {
140        lctxt.vars.put(varName, o);
141        for (LiquidNode n : body) {
142          n.evaluate(b, resource, lctxt);
143        }
144      }
145    }
146  }
147
148  private class LiquidInclude extends LiquidNode {
149    private String page;
150    private Map<String, ExpressionNode> params = new HashMap<>();
151
152    @Override
153    public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException {
154      String src = includeResolver.fetchInclude(LiquidEngine.this, page);
155      LiquidParser parser = new LiquidParser(src);
156      LiquidDocument doc = parser.parse(page);
157      LiquidEngineContext nctxt =  new LiquidEngineContext(ctxt.externalContext);
158      Tuple incl = new Tuple();
159      nctxt.vars.put("include", incl);
160      for (String s : params.keySet()) {
161        incl.addProperty(s, engine.evaluate(ctxt, resource, resource, params.get(s)));
162      }
163      for (LiquidNode n : doc.body) {
164        n.evaluate(b, resource, nctxt);
165      }
166    }
167  }
168
169  public static class LiquidDocument  {
170    private List<LiquidNode> body = new ArrayList<>();
171
172  }
173
174  private class LiquidParser {
175
176    private String source;
177    private int cursor;
178    private String name;
179
180    public LiquidParser(String source) {
181      this.source = source;
182      cursor = 0;
183    }
184
185    private char next1() {
186      if (cursor >= source.length())
187        return 0;
188      else
189        return source.charAt(cursor);
190    }
191
192    private char next2() {
193      if (cursor >= source.length()-1)
194        return 0;
195      else
196        return source.charAt(cursor+1);
197    }
198
199    private char grab() {
200      cursor++;
201      return source.charAt(cursor-1);
202    }
203
204    public LiquidDocument parse(String name) throws FHIRException {
205      this.name = name;
206      LiquidDocument doc = new LiquidDocument();
207      parseList(doc.body, new String[0]);
208      return doc;
209    }
210
211    private String parseList(List<LiquidNode> list, String[] terminators) throws FHIRException {
212      String close = null;
213      while (cursor < source.length()) {
214        if (next1() == '{' && (next2() == '%' || next2() == '{' )) {
215          if (next2() == '%') { 
216            String cnt = parseTag('%');
217            if (Utilities.existsInList(cnt, terminators)) {
218              close = cnt;
219              break;
220            } else if (cnt.startsWith("if "))
221              list.add(parseIf(cnt));
222            else if (cnt.startsWith("loop "))
223              list.add(parseLoop(cnt.substring(4).trim()));
224            else if (cnt.startsWith("include "))
225              list.add(parseInclude(cnt.substring(7).trim()));
226            else
227              throw new FHIRException("Script "+name+": Script "+name+": Unknown flow control statement "+cnt);
228          } else { // next2() == '{'
229            list.add(parseStatement());
230          }
231        } else {
232          if (list.size() == 0 || !(list.get(list.size()-1) instanceof LiquidConstant))
233            list.add(new LiquidConstant());
234          ((LiquidConstant) list.get(list.size()-1)).addChar(grab());
235        }
236      }
237      for (LiquidNode n : list)
238        n.closeUp();
239      if (terminators.length > 0)
240        if (!Utilities.existsInList(close, terminators))
241          throw new FHIRException("Script "+name+": Script "+name+": Found end of script looking for "+ Arrays.asList(terminators));
242      return close;
243    }
244
245    private LiquidNode parseIf(String cnt) throws FHIRException {
246      LiquidIf res = new LiquidIf();
247      res.condition = cnt.substring(3).trim();
248      String term = parseList(res.thenBody, new String[] { "else", "endif"} );
249      if ("else".equals(term))
250        term = parseList(res.elseBody, new String[] { "endif"} );
251      return res;
252    }
253
254    private LiquidNode parseInclude(String cnt) throws FHIRException {
255      int i = 1;
256      while (i < cnt.length() && !Character.isWhitespace(cnt.charAt(i)))
257        i++;
258      if (i == cnt.length() || i == 0)
259        throw new FHIRException("Script "+name+": Error reading include: "+cnt);
260      LiquidInclude res = new LiquidInclude();
261      res.page = cnt.substring(0, i);
262      while (i < cnt.length() && Character.isWhitespace(cnt.charAt(i)))
263        i++;
264      while (i < cnt.length()) {
265        int j = i;
266        while (i < cnt.length() && cnt.charAt(i) != '=')
267          i++;
268        if (i >= cnt.length() || j == i) 
269          throw new FHIRException("Script "+name+": Error reading include: "+cnt);
270        String n = cnt.substring(j, i);
271          if (res.params.containsKey(n)) 
272            throw new FHIRException("Script "+name+": Error reading include: "+cnt);
273          i++;
274          ExpressionNodeWithOffset t = engine.parsePartial(cnt, i);
275          i = t.getOffset();
276          res.params.put(n, t.getNode());
277          while (i < cnt.length() && Character.isWhitespace(cnt.charAt(i)))
278            i++;
279      }
280      return res;
281    }
282  
283
284    private LiquidNode parseLoop(String cnt) throws FHIRException {
285      int i = 0;
286      while (!Character.isWhitespace(cnt.charAt(i)))
287        i++;
288      LiquidLoop res = new LiquidLoop();
289      res.varName = cnt.substring(0, i);
290      while (Character.isWhitespace(cnt.charAt(i)))
291        i++;
292      int j = i;
293      while (!Character.isWhitespace(cnt.charAt(i)))
294        i++;
295      if (!"in".equals(cnt.substring(j, i)))
296        throw new FHIRException("Script "+name+": Script "+name+": Error reading loop: "+cnt);
297      res.condition = cnt.substring(i).trim();
298      parseList(res.body, new String[] { "endloop"} );
299      return res;
300    }
301
302    private String parseTag(char ch) throws FHIRException {
303      grab(); 
304      grab();
305      StringBuilder b = new StringBuilder();
306      while (cursor < source.length() && !(next1() == '%' && next2() == '}')) {
307        b.append(grab());
308      }
309      if (!(next1() == '%' && next2() == '}')) 
310        throw new FHIRException("Script "+name+": Unterminated Liquid statement {% "+b.toString());
311      grab(); 
312      grab();
313      return b.toString().trim();
314    }
315
316    private LiquidStatement parseStatement() throws FHIRException {
317      grab(); 
318      grab();
319      StringBuilder b = new StringBuilder();
320      while (cursor < source.length() && !(next1() == '}' && next2() == '}')) {
321        b.append(grab());
322      }
323      if (!(next1() == '}' && next2() == '}')) 
324        throw new FHIRException("Script "+name+": Unterminated Liquid statement {{ "+b.toString());
325      grab(); 
326      grab();
327      LiquidStatement res = new LiquidStatement();
328      res.statement = b.toString().trim();
329      return res;
330    }
331
332  }
333
334  @Override
335  public Base resolveConstant(Object appContext, String name, boolean beforeContext) throws PathEngineException {
336    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
337    if (ctxt.vars.containsKey(name))
338      return ctxt.vars.get(name);
339    if (externalHostServices == null)
340      return null;
341    return externalHostServices.resolveConstant(ctxt.externalContext, name, beforeContext);
342  }
343
344  @Override
345  public TypeDetails resolveConstantType(Object appContext, String name) throws PathEngineException {
346    if (externalHostServices == null)
347      return null;
348    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
349    return externalHostServices.resolveConstantType(ctxt.externalContext, name);
350  }
351
352  @Override
353  public boolean log(String argument, List<Base> focus) {
354    if (externalHostServices == null)
355      return false;
356    return externalHostServices.log(argument, focus);
357  }
358
359  @Override
360  public FunctionDetails resolveFunction(String functionName) {
361    if (externalHostServices == null)
362      return null;
363    return externalHostServices.resolveFunction(functionName);
364  }
365
366  @Override
367  public TypeDetails checkFunction(Object appContext, String functionName, List<TypeDetails> parameters) throws PathEngineException {
368    if (externalHostServices == null)
369      return null;
370    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
371    return externalHostServices.checkFunction(ctxt.externalContext, functionName, parameters);
372  }
373
374  @Override
375  public List<Base> executeFunction(Object appContext, String functionName, List<List<Base>> parameters) {
376    if (externalHostServices == null)
377      return null;
378    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
379    return externalHostServices.executeFunction(ctxt.externalContext, functionName, parameters);
380  }
381
382  @Override
383  public Base resolveReference(Object appContext, String url) throws FHIRException {
384    if (externalHostServices == null)
385      return null;
386    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
387    return resolveReference(ctxt.externalContext, url);
388  }
389
390  @Override
391  public boolean conformsToProfile(Object appContext, Base item, String url) throws FHIRException {
392    if (externalHostServices == null)
393      return false;
394    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
395    return conformsToProfile(ctxt.externalContext, item, url);
396  }
397
398}