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