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