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