001package org.hl7.fhir.r5.utils;
002
003import java.util.ArrayList;
004import java.util.Arrays;
005import java.util.Collections;
006import java.util.HashMap;
007import java.util.List;
008import java.util.Map;
009
010/*
011  Copyright (c) 2011+, HL7, Inc.
012  All rights reserved.
013  
014  Redistribution and use in source and binary forms, with or without modification, 
015  are permitted provided that the following conditions are met:
016    
017   * Redistributions of source code must retain the above copyright notice, this 
018     list of conditions and the following disclaimer.
019   * Redistributions in binary form must reproduce the above copyright notice, 
020     this list of conditions and the following disclaimer in the documentation 
021     and/or other materials provided with the distribution.
022   * Neither the name of HL7 nor the names of its contributors may be used to 
023     endorse or promote products derived from this software without specific 
024     prior written permission.
025  
026  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
027  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
028  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
029  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
030  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
031  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
032  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
033  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
034  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
035  POSSIBILITY OF SUCH DAMAGE.
036  
037 */
038
039import org.hl7.fhir.exceptions.FHIRException;
040import org.hl7.fhir.exceptions.PathEngineException;
041import org.hl7.fhir.r5.context.IWorkerContext;
042import org.hl7.fhir.r5.fhirpath.ExpressionNode;
043import org.hl7.fhir.r5.fhirpath.FHIRLexer;
044import org.hl7.fhir.r5.fhirpath.FHIRPathEngine;
045import org.hl7.fhir.r5.fhirpath.TypeDetails;
046import org.hl7.fhir.r5.fhirpath.FHIRPathEngine.ExpressionNodeWithOffset;
047import org.hl7.fhir.r5.fhirpath.FHIRPathEngine.IEvaluationContext;
048import org.hl7.fhir.r5.fhirpath.FHIRPathUtilityClasses.FunctionDetails;
049import org.hl7.fhir.r5.model.Base;
050import org.hl7.fhir.r5.model.Tuple;
051import org.hl7.fhir.r5.model.ValueSet;
052import org.hl7.fhir.utilities.Utilities;
053import org.hl7.fhir.utilities.i18n.I18nConstants;
054import org.hl7.fhir.utilities.xhtml.NodeType;
055import org.hl7.fhir.utilities.xhtml.XhtmlNode;
056
057public class LiquidEngine implements IEvaluationContext {
058
059  public interface ILiquidRenderingSupport {
060    String renderForLiquid(Object appContext, Base i) throws FHIRException;
061  }
062
063  public interface ILiquidEngineIncludeResolver {
064    public String fetchInclude(LiquidEngine engine, String name);
065  }
066
067  private IEvaluationContext externalHostServices;
068  private FHIRPathEngine engine;
069  private ILiquidEngineIncludeResolver includeResolver;
070  private ILiquidRenderingSupport renderingSupport;
071
072  private class LiquidEngineContext {
073    private Object externalContext;
074    private Map<String, Base> loopVars = new HashMap<>();
075    private Map<String, Base> globalVars = new HashMap<>();
076
077    public LiquidEngineContext(Object externalContext) {
078      super();
079      this.externalContext = externalContext;
080      globalVars = new HashMap<>();
081    }
082
083    public LiquidEngineContext(Object externalContext, LiquidEngineContext existing) {
084      super();
085      this.externalContext = externalContext;
086      loopVars.putAll(existing.loopVars);
087      globalVars = existing.globalVars;
088    }
089
090    public LiquidEngineContext(LiquidEngineContext existing) {
091      super();
092      externalContext = existing.externalContext;
093      loopVars.putAll(existing.loopVars);
094      globalVars = existing.globalVars;
095    }
096  }
097
098  public LiquidEngine(IWorkerContext context, IEvaluationContext hostServices) {
099    super();
100    this.externalHostServices = hostServices;
101    engine = new FHIRPathEngine(context);
102    engine.setHostServices(this);
103    engine.setLiquidMode(true);
104  }
105
106  public ILiquidEngineIncludeResolver getIncludeResolver() {
107    return includeResolver;
108  }
109
110  public void setIncludeResolver(ILiquidEngineIncludeResolver includeResolver) {
111    this.includeResolver = includeResolver;
112  }
113
114  public ILiquidRenderingSupport getRenderingSupport() {
115    return renderingSupport;
116  }
117
118  public void setRenderingSupport(ILiquidRenderingSupport renderingSupport) {
119    this.renderingSupport = renderingSupport;
120  }
121
122  public LiquidDocument parse(String source, String sourceName) throws FHIRException {
123    return new LiquidParser(source).parse(sourceName);
124  }
125
126  public String evaluate(LiquidDocument document, Base resource, Object appContext) throws FHIRException {
127    StringBuilder b = new StringBuilder();
128    LiquidEngineContext ctxt = new LiquidEngineContext(appContext);
129    for (LiquidNode n : document.body) {
130      n.evaluate(b, resource, ctxt);
131    }
132    return b.toString();
133  }
134  
135
136  private abstract class LiquidNode {
137    protected void closeUp() {
138    }
139
140    public abstract void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException;
141  }
142
143  private class LiquidConstant extends LiquidNode {
144    private String constant;
145    private StringBuilder b = new StringBuilder();
146
147    @Override
148    protected void closeUp() {
149      constant = b.toString();
150      b = null;
151    }
152
153    public void addChar(char ch) {
154      b.append(ch);
155    }
156
157    @Override
158    public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) {
159      b.append(constant);
160    }
161
162  }
163  
164  private enum LiquidFilter {
165    PREPEND;
166    
167    public static LiquidFilter fromCode(String code) {
168      if ("prepend".equals(code)) {
169        return PREPEND;
170      }
171      return null;
172    }
173  }
174
175  private class LiquidExpressionNode {
176    private LiquidFilter filter; // null at root
177    private ExpressionNode expression; // null for some filters
178    public LiquidExpressionNode(LiquidFilter filter, ExpressionNode expression) {
179      super();
180      this.filter = filter;
181      this.expression = expression;
182    }
183    
184  }
185  
186  private class LiquidStatement extends LiquidNode {
187    private String statement;
188    private List<LiquidExpressionNode> compiled = new ArrayList<>();
189
190    @Override
191    public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
192      if (compiled.size() == 0) {
193        FHIRLexer lexer = new FHIRLexer(statement, "liquid statement", false, true);
194        lexer.setLiquidMode(true);
195        compiled.add(new LiquidExpressionNode(null, engine.parse(lexer)));
196        while (!lexer.done()) {
197          if (lexer.getCurrent().equals("||")) {
198            lexer.next();
199            String f = lexer.getCurrent();
200            LiquidFilter filter = LiquidFilter.fromCode(f);
201            if (filter == null) {
202              lexer.error(engine.getWorker().formatMessage(I18nConstants.LIQUID_UNKNOWN_FILTER, f));
203            }
204            lexer.next();
205            if (!lexer.done() && lexer.getCurrent().equals(":")) {
206              lexer.next();
207              compiled.add(new LiquidExpressionNode(filter, engine.parse(lexer)));
208            } else {
209              compiled.add(new LiquidExpressionNode(filter, null));
210            }
211          } else {
212            lexer.error(engine.getWorker().formatMessage(I18nConstants.LIQUID_UNKNOWN_SYNTAX)); 
213          }
214        }
215      }
216      
217      String t = null;
218      for (LiquidExpressionNode i : compiled) {
219        if (i.filter == null) { // first
220          t = stmtToString(ctxt, engine.evaluate(ctxt, resource, resource, resource, i.expression));
221        } else switch (i.filter) {
222        case PREPEND:
223          t = stmtToString(ctxt, engine.evaluate(ctxt, resource, resource, resource, i.expression)) + t;
224          break;
225        }
226      }
227      b.append(t);
228    }
229
230    private String stmtToString(LiquidEngineContext ctxt, List<Base> items) {
231      StringBuilder b = new StringBuilder();
232      boolean first = true;
233      for (Base i : items) {
234        if (i != null) {
235          if (first) first = false; else b.append(", ");
236          String s = renderingSupport != null ? renderingSupport.renderForLiquid(ctxt.externalContext, i) : null;
237          b.append(s != null ? s : engine.convertToString(i));
238        }
239      }
240      return b.toString();
241    }
242  }
243
244  private class LiquidElsIf extends LiquidNode {
245    private String condition;
246    private ExpressionNode compiled;
247    private List<LiquidNode> body = new ArrayList<>();
248
249    @Override
250    public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
251      for (LiquidNode n : body) {
252        n.evaluate(b, resource, ctxt);
253      }
254    }
255  }
256
257  private class LiquidIf extends LiquidNode {
258    private String condition;
259    private ExpressionNode compiled;
260    private List<LiquidNode> thenBody = new ArrayList<>();
261    private List<LiquidElsIf> elseIf = new ArrayList<>();
262    private List<LiquidNode> elseBody = new ArrayList<>();
263
264    @Override
265    public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
266      if (compiled == null)
267        compiled = engine.parse(condition);
268      boolean ok = engine.evaluateToBoolean(ctxt, resource, resource, resource, compiled);
269      List<LiquidNode> list = null;
270      if (ok) {
271        list = thenBody;
272
273      } else {
274        list = elseBody;
275        for (LiquidElsIf i : elseIf) {
276          if (i.compiled == null)
277            i.compiled = engine.parse(i.condition);
278          ok = engine.evaluateToBoolean(ctxt, resource, resource, resource, i.compiled);
279          if (ok) {
280            list = i.body;
281            break;
282          }
283        }
284      }
285      for (LiquidNode n : list) {
286        n.evaluate(b, resource, ctxt);
287      }
288    }
289  }
290
291  private class LiquidContinueExecuted extends FHIRException {
292    private static final long serialVersionUID = 4748737094188943721L;
293  }
294
295  private class LiquidContinue extends LiquidNode {
296    public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
297      throw new LiquidContinueExecuted();
298    }
299  }
300
301  private class LiquidBreakExecuted extends FHIRException {
302    private static final long serialVersionUID = 6328496371172871082L;
303  }
304
305  private class LiquidBreak extends LiquidNode {
306    public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
307      throw new LiquidBreakExecuted();
308    }
309  }
310
311  private class LiquidCycle extends LiquidNode {
312    private List<String> list = new ArrayList<>();
313    private int cursor = 0;
314
315    public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
316      b.append(list.get(cursor));
317      cursor++;
318      if (cursor == list.size()) {
319        cursor = 0;
320      }
321    }
322  }
323
324  private class LiquidAssign extends LiquidNode {
325    private String varName;
326    private String expression;
327    private ExpressionNode compiled;
328    @Override
329    public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
330      if (compiled == null) {
331        boolean dbl = engine.isAllowDoubleQuotes();
332        engine.setAllowDoubleQuotes(true);
333        ExpressionNodeWithOffset po = engine.parsePartial(expression, 0);
334        compiled = po.getNode();
335        engine.setAllowDoubleQuotes(dbl);
336      }
337      List<Base> list = engine.evaluate(ctxt, resource, resource, resource, compiled);
338      if (list.isEmpty()) {
339        ctxt.globalVars.remove(varName);
340      } else if (list.size() == 1) {
341        ctxt.globalVars.put(varName, list.get(0));
342      } else {
343        throw new Error("Assign returned a list?");
344      }      
345    }    
346  }
347  
348  private class LiquidFor extends LiquidNode {
349    private String varName;
350    private String condition;
351    private ExpressionNode compiled;
352    private boolean reversed = false;
353    private int limit = -1;
354    private int offset = -1;
355    private List<LiquidNode> body = new ArrayList<>();
356    private List<LiquidNode> elseBody = new ArrayList<>();
357
358    @Override
359    public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
360      if (compiled == null) {
361        ExpressionNodeWithOffset po = engine.parsePartial(condition, 0);
362        compiled = po.getNode();
363        if (po.getOffset() < condition.length()) {
364          parseModifiers(condition.substring(po.getOffset()));
365        }
366      }
367      List<Base> list = engine.evaluate(ctxt, resource, resource, resource, compiled);
368      LiquidEngineContext lctxt = new LiquidEngineContext(ctxt);
369      if (list.isEmpty()) {
370        for (LiquidNode n : elseBody) {
371          n.evaluate(b, resource, lctxt);
372        }
373      } else {
374        if (reversed) {
375          Collections.reverse(list);
376        }
377        int i = 0;
378        for (Base o : list) {
379          if (offset >= 0 && i < offset) {
380            i++;
381            continue;
382          }
383          if (limit >= 0 && i == limit) {
384            break;
385          }          
386          if (lctxt.globalVars.containsKey(varName)) {
387            throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_VARIABLE_ALREADY_ASSIGNED, varName));
388          }
389          lctxt.loopVars.put(varName, o);
390          boolean wantBreak = false;
391          for (LiquidNode n : body) {
392            try {
393              n.evaluate(b, resource, lctxt);
394            } catch (LiquidContinueExecuted e) {
395              break;
396            } catch (LiquidBreakExecuted e) {
397              wantBreak = true;
398              break;
399            }
400          }
401          if (wantBreak) {
402            break;
403          }
404          i++;
405        }
406      }
407    }
408
409    private void parseModifiers(String cnt) {
410      String src = cnt;
411      while (!Utilities.noString(cnt)) {
412        if (cnt.startsWith("reversed")) {
413          reversed = true;
414          cnt = cnt.substring(8);
415        } else if (cnt.startsWith("limit")) {
416          cnt = cnt.substring(5).trim();
417          if (!cnt.startsWith(":")) {
418            throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_COLON, src));
419          }
420          cnt = cnt.substring(1).trim();
421          int i = 0;
422          while (i < cnt.length() && Character.isDigit(cnt.charAt(i))) {
423            i++;
424          }
425          if (i == 0) {
426            throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_NUMBER, src));
427          }
428          limit = Integer.parseInt(cnt.substring(0, i));
429          cnt = cnt.substring(i);
430        } else if (cnt.startsWith("offset")) {
431          cnt = cnt.substring(6).trim();
432          if (!cnt.startsWith(":")) {
433            throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_COLON, src));
434          }
435          cnt = cnt.substring(1).trim();
436          int i = 0;
437          while (i < cnt.length() && Character.isDigit(cnt.charAt(i))) {
438            i++;
439          }
440          if (i == 0) {
441            throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_NUMBER, src));
442          }
443          offset = Integer.parseInt(cnt.substring(0, i));
444          cnt = cnt.substring(i);
445        } else {
446          throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_UNEXPECTED, cnt));
447        }
448      }      
449    }
450  }
451
452  private class LiquidInclude extends LiquidNode {
453    private String page;
454    private Map<String, ExpressionNode> params = new HashMap<>();
455
456    @Override
457    public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
458      if (includeResolver == null) {
459        throw new FHIRException("Includes are not supported in this context");
460      }
461      String src = includeResolver.fetchInclude(LiquidEngine.this, page);
462      if (src == null) {
463        throw new FHIRException("The include '"+page+"' could not be resolved");
464      }
465      LiquidParser parser = new LiquidParser(src);
466      LiquidDocument doc = parser.parse(page);
467      LiquidEngineContext nctxt = new LiquidEngineContext(ctxt.externalContext, ctxt);
468      Tuple incl = new Tuple();
469      nctxt.loopVars.put("include", incl);
470      for (String s : params.keySet()) {
471        incl.addProperty(s, engine.evaluate(ctxt, resource, resource, resource, params.get(s)));
472      }
473      for (LiquidNode n : doc.body) {
474        n.evaluate(b, resource, nctxt);
475      }
476    }
477  }
478
479  public static class LiquidDocument {
480    private List<LiquidNode> body = new ArrayList<>();
481
482  }
483
484  private class LiquidParser {
485
486    private String source;
487    private int cursor;
488    private String name;
489
490    public LiquidParser(String source) {
491      this.source = source;
492      cursor = 0;
493    }
494
495    private char next1() {
496      if (cursor >= source.length())
497        return 0;
498      else
499        return source.charAt(cursor);
500    }
501
502    private char next2() {
503      if (cursor >= source.length() - 1)
504        return 0;
505      else
506        return source.charAt(cursor + 1);
507    }
508
509    private char grab() {
510      cursor++;
511      return source.charAt(cursor - 1);
512    }
513
514    public LiquidDocument parse(String name) throws FHIRException {
515      this.name = name;
516      LiquidDocument doc = new LiquidDocument();
517      parseList(doc.body, false, new String[0]);
518      return doc;
519    }
520
521    public LiquidCycle parseCycle(String cnt) {
522      LiquidCycle res = new LiquidCycle();
523      cnt = "," + cnt.substring(5).trim();
524      while (!Utilities.noString(cnt)) {
525        if (!cnt.startsWith(",")) {
526          throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_EXPECTING, name, cnt.charAt(0), ','));
527        }
528        cnt = cnt.substring(1).trim();
529        if (!cnt.startsWith("\"")) {
530          throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_EXPECTING, name, cnt.charAt(0), '"'));
531        }
532        cnt = cnt.substring(1);
533        int i = 0;
534        while (i < cnt.length() && cnt.charAt(i) != '"') {
535          i++;
536        }
537        if (i == cnt.length()) {
538          throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_UNTERMINATED, name));
539        }
540        res.list.add(cnt.substring(0, i));
541        cnt = cnt.substring(i + 1).trim();
542      }
543      return res;
544    }
545
546    private String parseList(List<LiquidNode> list, boolean inLoop, String[] terminators) throws FHIRException {
547      String close = null;
548      while (cursor < source.length()) {
549        if (next1() == '{' && (next2() == '%' || next2() == '{')) {
550          if (next2() == '%') {
551            String cnt = parseTag('%');
552            if (isTerminator(cnt, terminators)) {
553              close = cnt;
554              break;
555            } else if (cnt.startsWith("if "))
556              list.add(parseIf(cnt, inLoop));
557            else if (cnt.startsWith("loop ")) // loop is deprecated, but still
558                                              // supported
559              list.add(parseLoop(cnt.substring(4).trim()));
560            else if (cnt.startsWith("for "))
561              list.add(parseFor(cnt.substring(3).trim()));
562            else if (inLoop && cnt.equals("continue"))
563              list.add(new LiquidContinue());
564            else if (inLoop && cnt.equals("break"))
565              list.add(new LiquidBreak());
566            else if (inLoop && cnt.startsWith("cycle "))
567              list.add(parseCycle(cnt));
568            else if (cnt.startsWith("include "))
569              list.add(parseInclude(cnt.substring(7).trim()));
570            else if (cnt.startsWith("assign "))
571              list.add(parseAssign(cnt.substring(6).trim()));
572            else
573              throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_UNKNOWN_FLOW_STMT,name, cnt));
574          } else { // next2() == '{'
575            list.add(parseStatement());
576          }
577        } else {
578          if (list.size() == 0 || !(list.get(list.size() - 1) instanceof LiquidConstant))
579            list.add(new LiquidConstant());
580          ((LiquidConstant) list.get(list.size() - 1)).addChar(grab());
581        }
582      }
583      for (LiquidNode n : list)
584        n.closeUp();
585      if (terminators.length > 0)
586        if (!isTerminator(close, terminators))
587          throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_UNKNOWN_NOEND, name, terminators));
588      return close;
589    }
590
591    private boolean isTerminator(String cnt, String[] terminators) {
592      if (Utilities.noString(cnt)) {
593        return false;
594      }
595      for (String t : terminators) {
596        if (t.endsWith(" ")) {
597          if (cnt.startsWith(t)) {
598            return true;
599          }
600        } else {
601          if (cnt.equals(t)) {
602            return true;
603          }
604        }
605      }
606      return false;
607    }
608
609    private LiquidNode parseIf(String cnt, boolean inLoop) throws FHIRException {
610      LiquidIf res = new LiquidIf();
611      res.condition = cnt.substring(3).trim();
612      String term = parseList(res.thenBody, inLoop, new String[] { "else", "elsif ", "endif" });
613      while (term.startsWith("elsif ")) {
614        LiquidElsIf elsIf = new LiquidElsIf();
615        res.elseIf.add(elsIf);
616        elsIf.condition = term.substring(5).trim();
617        term = parseList(elsIf.body, inLoop, new String[] { "elsif ", "else", "endif" });
618      }
619      if ("else".equals(term)) {
620        term = parseList(res.elseBody, inLoop, new String[] { "endif" });
621      }
622
623      return res;
624    }
625
626    private LiquidNode parseInclude(String cnt) throws FHIRException {
627      int i = 1;
628      while (i < cnt.length() && !Character.isWhitespace(cnt.charAt(i)))
629        i++;
630      if (i == 0)
631        throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_INCLUDE, name, cnt));
632      LiquidInclude res = new LiquidInclude();
633      res.page = cnt.substring(0, i);
634      while (i < cnt.length() && Character.isWhitespace(cnt.charAt(i)))
635        i++;
636      while (i < cnt.length()) {
637        int j = i;
638        while (i < cnt.length() && cnt.charAt(i) != '=')
639          i++;
640        if (i >= cnt.length() || j == i)
641          throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_INCLUDE, name,  cnt));
642        String n = cnt.substring(j, i);
643        if (res.params.containsKey(n))
644          throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_INCLUDE, name,  cnt));
645        i++;
646        ExpressionNodeWithOffset t = engine.parsePartial(cnt, i);
647        i = t.getOffset();
648        res.params.put(n, t.getNode());
649        while (i < cnt.length() && Character.isWhitespace(cnt.charAt(i)))
650          i++;
651      }
652      return res;
653    }
654
655    private LiquidNode parseLoop(String cnt) throws FHIRException {
656      int i = 0;
657      while (!Character.isWhitespace(cnt.charAt(i)))
658        i++;
659      LiquidFor res = new LiquidFor();
660      res.varName = cnt.substring(0, i);
661      if ("include".equals(res.varName)) {
662        throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_VARIABLE_ILLEGAL, res.varName));
663      }
664      while (Character.isWhitespace(cnt.charAt(i)))
665        i++;
666      int j = i;
667      while (!Character.isWhitespace(cnt.charAt(i)))
668        i++;
669      if (!"in".equals(cnt.substring(j, i)))
670        throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_LOOP, name, cnt));
671      res.condition = cnt.substring(i).trim();
672      parseList(res.body, false, new String[] { "endloop" });
673      return res;
674    }
675
676    private LiquidNode parseFor(String cnt) throws FHIRException {
677      int i = 0;
678      while (!Character.isWhitespace(cnt.charAt(i)))
679        i++;
680      LiquidFor res = new LiquidFor();
681      res.varName = cnt.substring(0, i);
682      if ("include".equals(res.varName)) {
683        throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_VARIABLE_ILLEGAL, res.varName));
684      }
685      while (Character.isWhitespace(cnt.charAt(i)))
686        i++;
687      int j = i;
688      while (!Character.isWhitespace(cnt.charAt(i)))
689        i++;
690      if (!"in".equals(cnt.substring(j, i)))
691        throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_LOOP, name, cnt));
692      res.condition = cnt.substring(i).trim();
693      String term = parseList(res.body, true, new String[] { "endfor", "else" });
694      if ("else".equals(term)) {
695        parseList(res.elseBody, false, new String[] { "endfor" });
696      }
697      return res;
698    }
699
700    private LiquidNode parseAssign(String cnt) throws FHIRException {
701      int i = 0;
702      while (!Character.isWhitespace(cnt.charAt(i)))
703        i++;
704      LiquidAssign res = new LiquidAssign();
705      res.varName = cnt.substring(0, i);
706      while (Character.isWhitespace(cnt.charAt(i)))
707        i++;
708      int j = i;
709      while (!Character.isWhitespace(cnt.charAt(i)))
710        i++;
711      res.expression = cnt.substring(i).trim();
712      return res;
713    }
714
715    private String parseTag(char ch) throws FHIRException {
716      grab();
717      grab();
718      StringBuilder b = new StringBuilder();
719      while (cursor < source.length() && !(next1() == '%' && next2() == '}')) {
720        b.append(grab());
721      }
722      if (!(next1() == '%' && next2() == '}'))
723        throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_NOTERM, name,  "{% " + b.toString()));
724      grab();
725      grab();
726      return b.toString().trim();
727    }
728
729    private LiquidStatement parseStatement() throws FHIRException {
730      grab();
731      grab();
732      StringBuilder b = new StringBuilder();
733      while (cursor < source.length() && !(next1() == '}' && next2() == '}')) {
734        b.append(grab());
735      }
736      if (!(next1() == '}' && next2() == '}'))
737        throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_NOTERM, name,  "{{ " + b.toString()));
738      grab();
739      grab();
740      LiquidStatement res = new LiquidStatement();
741      res.statement = b.toString().trim();
742      return res;
743    }
744
745  }
746
747  @Override
748  public List<Base> resolveConstant(FHIRPathEngine engine, Object appContext, String name, boolean beforeContext, boolean explicitConstant) throws PathEngineException {
749    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
750    if (ctxt.loopVars.containsKey(name))
751      return new ArrayList<Base>(Arrays.asList(ctxt.loopVars.get(name)));
752    if (ctxt.globalVars.containsKey(name))
753      return new ArrayList<Base>(Arrays.asList(ctxt.globalVars.get(name)));
754    if (externalHostServices == null)
755      return new ArrayList<Base>();
756    return externalHostServices.resolveConstant(engine, ctxt.externalContext, name, beforeContext, explicitConstant);
757  }
758
759  @Override
760  public TypeDetails resolveConstantType(FHIRPathEngine engine, Object appContext, String name, boolean explicitConstant) throws PathEngineException {
761    if (externalHostServices == null)
762      return null;
763    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
764    return externalHostServices.resolveConstantType(engine, ctxt.externalContext, name, explicitConstant);
765  }
766
767  @Override
768  public boolean log(String argument, List<Base> focus) {
769    if (externalHostServices == null)
770      return false;
771    return externalHostServices.log(argument, focus);
772  }
773
774  @Override
775  public FunctionDetails resolveFunction(FHIRPathEngine engine, String functionName) {
776    if (externalHostServices == null)
777      return null;
778    return externalHostServices.resolveFunction(engine, functionName);
779  }
780
781  @Override
782  public TypeDetails checkFunction(FHIRPathEngine engine, Object appContext, String functionName, TypeDetails focus, List<TypeDetails> parameters) throws PathEngineException {
783    if (externalHostServices == null)
784      return null;
785    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
786    return externalHostServices.checkFunction(engine, ctxt.externalContext, functionName, focus, parameters);
787  }
788
789  @Override
790  public List<Base> executeFunction(FHIRPathEngine engine, Object appContext, List<Base> focus, String functionName, List<List<Base>> parameters) {
791    if (externalHostServices == null)
792      return null;
793    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
794    return externalHostServices.executeFunction(engine, ctxt.externalContext, focus, functionName, parameters);
795  }
796
797  @Override
798  public Base resolveReference(FHIRPathEngine engine, Object appContext, String url, Base refContext) throws FHIRException {
799    if (externalHostServices == null)
800      return null;
801    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
802    return resolveReference(engine, ctxt.externalContext, url, refContext);
803  }
804
805  @Override
806  public boolean conformsToProfile(FHIRPathEngine engine, Object appContext, Base item, String url) throws FHIRException {
807    if (externalHostServices == null)
808      return false;
809    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
810    return conformsToProfile(engine, ctxt.externalContext, item, url);
811  }
812
813  @Override
814  public ValueSet resolveValueSet(FHIRPathEngine engine, Object appContext, String url) {
815    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
816    if (externalHostServices != null)
817      return externalHostServices.resolveValueSet(engine, ctxt.externalContext, url);
818    else
819      return engine.getWorker().fetchResource(ValueSet.class, url);
820  }
821
822  /**
823   * Lightweight method to replace fixed constants in resources
824   * 
825   * @param node
826   * @param vars
827   * @return
828   */
829  public boolean replaceInHtml(XhtmlNode node, Map<String, String> vars) {
830    boolean replaced = false;
831    if (node.getNodeType() == NodeType.Text || node.getNodeType() == NodeType.Comment) {
832      String cnt = node.getContent();
833      for (String n : vars.keySet()) {
834        cnt = cnt.replace(n, vars.get(n));
835      }
836      if (!cnt.equals(node.getContent())) {
837        node.setContent(cnt);
838        replaced = true;
839      }
840    } else if (node.getNodeType() == NodeType.Element || node.getNodeType() == NodeType.Document) {
841      for (XhtmlNode c : node.getChildNodes()) {
842        if (replaceInHtml(c, vars)) {
843          replaced = true;
844        }
845      }
846      for (String an : node.getAttributes().keySet()) {
847        String cnt = node.getAttributes().get(an);
848        for (String n : vars.keySet()) {
849          cnt = cnt.replace(n, vars.get(n));
850        }
851        if (!cnt.equals(node.getAttributes().get(an))) {
852          node.getAttributes().put(an, cnt);
853          replaced = true;
854        }
855      }
856    }
857    return replaced;
858  }
859
860  @Override
861  public boolean paramIsType(String name, int index) {
862    return false;
863  }
864}