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