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