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