001package org.hl7.fhir.r5.liquid;
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.FHIRPathEngine.ExpressionNodeWithOffset;
046import org.hl7.fhir.r5.fhirpath.FHIRPathEngine.IEvaluationContext;
047import org.hl7.fhir.r5.fhirpath.FHIRPathUtilityClasses.FunctionDetails;
048import org.hl7.fhir.r5.fhirpath.TypeDetails;
049import org.hl7.fhir.r5.fhirpath.ExpressionNode.CollectionStatus;
050import org.hl7.fhir.r5.model.Base;
051import org.hl7.fhir.r5.model.BooleanType;
052import org.hl7.fhir.r5.model.IdType;
053import org.hl7.fhir.r5.model.IntegerType;
054import org.hl7.fhir.r5.model.StringType;
055import org.hl7.fhir.r5.model.Tuple;
056import org.hl7.fhir.r5.model.ValueSet;
057import org.hl7.fhir.utilities.FhirPublication;
058import org.hl7.fhir.utilities.KeyIssuer;
059import org.hl7.fhir.utilities.MarkDownProcessor;
060import org.hl7.fhir.utilities.MarkedToMoveToAdjunctPackage;
061import org.hl7.fhir.utilities.MarkDownProcessor.Dialect;
062import org.hl7.fhir.utilities.Utilities;
063import org.hl7.fhir.utilities.i18n.I18nConstants;
064import org.hl7.fhir.utilities.xhtml.NodeType;
065import org.hl7.fhir.utilities.xhtml.XhtmlNode;
066
067@MarkedToMoveToAdjunctPackage
068public class LiquidEngine implements IEvaluationContext {
069
070  public static class LiquidForLoopObject extends Base {
071
072    private static final long serialVersionUID = 6951452522873320076L;
073    private boolean first;
074    private int index;
075    private int index0;
076    private int rindex;
077    private int rindex0;
078    private boolean last;
079    private int length;
080    private LiquidForLoopObject parentLoop;
081    
082    
083    public LiquidForLoopObject(int size, int i, int offset, int limit, LiquidForLoopObject parentLoop) {
084      super();
085      this.parentLoop = parentLoop;
086      if (offset == -1) {
087        offset = 0;
088      }
089      if (limit == -1) {
090        limit = size;
091      }
092      
093      first = i == offset;
094      index = i+1-offset;
095      index0 = i-offset;
096      rindex = (limit-offset) - 1 - i;
097      rindex0 = (limit-offset) - i;
098      length = limit-offset;
099      last = i == (limit-offset)-1;                
100    }
101    
102
103    @Override
104    public String getIdBase() {
105      return null;
106    }
107
108    @Override
109    public void setIdBase(String value) {
110      throw new Error("forLoop is read only");
111    }
112
113    @Override
114    public Base copy() {
115      throw new Error("forLoop is read only");
116    }
117
118    @Override
119    public FhirPublication getFHIRPublicationVersion() {
120      return FhirPublication.R5;
121    }
122
123    public Base[] getProperty(int hash, String name, boolean checkValid) throws FHIRException {
124      switch (name) {
125      case "parentLoop" : return wrap(parentLoop);
126      case "first" : return wrap(new BooleanType(first));
127      case "last" : return wrap(new BooleanType(last));
128      case "index" : return wrap(new IntegerType(index));
129      case "index0" : return wrap(new IntegerType(index0));
130      case "rindex" : return wrap(new IntegerType(rindex));
131      case "rindex0" : return wrap(new IntegerType(rindex0));
132      case "length" : return wrap(new IntegerType(length));
133      }
134
135      return super.getProperty(hash, name, checkValid);
136    }
137
138    private Base[] wrap(Base b) {
139      Base[] l = new Base[1];
140      l[0] = b;
141      return l;
142    }
143
144
145    @Override
146    public String toString() {
147      return "forLoop";
148    }
149
150
151    @Override
152    public String fhirType() {
153      return "ForLoop";
154    }
155    
156  }
157
158  public interface ILiquidRenderingSupport {
159    String renderForLiquid(Object appContext, Base i) throws FHIRException;
160  }
161
162  public interface ILiquidEngineIncludeResolver {
163    public String fetchInclude(LiquidEngine engine, String name);
164  }
165
166  private IEvaluationContext externalHostServices;
167  private FHIRPathEngine engine;
168  private ILiquidEngineIncludeResolver includeResolver;
169  private ILiquidRenderingSupport renderingSupport;
170  private MarkDownProcessor processor = new MarkDownProcessor(Dialect.COMMON_MARK);
171  private Map<String, Base> vars = new HashMap<>();
172  private KeyIssuer keyIssuer;
173  
174  private class LiquidEngineContext {
175    private Object externalContext;
176    private Map<String, Base> loopVars = new HashMap<>();
177    private Map<String, Base> globalVars = new HashMap<>();
178
179    public LiquidEngineContext(Object externalContext, Map<String, Base> vars) {
180      super();
181      this.externalContext = externalContext;
182      globalVars = new HashMap<>();
183      globalVars.putAll(vars);
184    }
185
186    public LiquidEngineContext(Object externalContext, LiquidEngineContext existing) {
187      super();
188      this.externalContext = externalContext;
189      loopVars.putAll(existing.loopVars);
190      globalVars = existing.globalVars;
191    }
192
193    public LiquidEngineContext(LiquidEngineContext existing) {
194      super();
195      externalContext = existing.externalContext;
196      loopVars.putAll(existing.loopVars);
197      globalVars = existing.globalVars;
198    }
199  }
200
201  public LiquidEngine(IWorkerContext context, IEvaluationContext hostServices) {
202    super();
203    this.externalHostServices = hostServices;
204    engine = new FHIRPathEngine(context);
205    engine.setHostServices(this);
206    engine.setLiquidMode(true);
207  }
208
209  public ILiquidEngineIncludeResolver getIncludeResolver() {
210    return includeResolver;
211  }
212
213  public void setIncludeResolver(ILiquidEngineIncludeResolver includeResolver) {
214    this.includeResolver = includeResolver;
215  }
216
217  public ILiquidRenderingSupport getRenderingSupport() {
218    return renderingSupport;
219  }
220
221  public void setRenderingSupport(ILiquidRenderingSupport renderingSupport) {
222    this.renderingSupport = renderingSupport;
223  }
224
225  public Map<String, Base> getVars() {
226    return vars;
227  }
228
229  public LiquidDocument parse(String source, String sourceName) throws FHIRException {
230    return new LiquidParser(source).parse(sourceName);
231  }
232
233  public String evaluate(LiquidDocument document, Base resource, Object appContext) throws FHIRException {
234    StringBuilder b = new StringBuilder();
235    LiquidEngineContext ctxt = new LiquidEngineContext(appContext, vars );
236    for (LiquidNode n : document.body) {
237      n.evaluate(b, resource, ctxt);
238    }
239    return b.toString();
240  }
241  
242
243  private abstract class LiquidNode {
244    protected void closeUp() {
245    }
246
247    public abstract void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException;
248  }
249
250  private class LiquidConstant extends LiquidNode {
251    private String constant;
252    private StringBuilder b = new StringBuilder();
253
254    @Override
255    protected void closeUp() {
256      constant = b.toString();
257      b = null;
258    }
259
260    public void addChar(char ch) {
261      b.append(ch);
262    }
263
264    @Override
265    public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) {
266      b.append(constant);
267    }
268
269  }
270  
271  private enum LiquidFilter {
272    PREPEND,
273    MARKDOWNIFY,
274    UPCASE,
275    DOWNCASE;
276    
277    public static LiquidFilter fromCode(String code) {
278      if ("prepend".equals(code)) {
279        return PREPEND;
280      }
281      if ("markdownify".equals(code)) {
282        return MARKDOWNIFY;
283      }
284      if ("upcase".equals(code)) {
285        return UPCASE;
286      }
287      if ("downcase".equals(code)) {
288        return DOWNCASE;
289      }
290      return null;
291    }
292  }
293
294  private class LiquidExpressionNode {
295    private LiquidFilter filter; // null at root
296    private ExpressionNode expression; // null for some filters
297    public LiquidExpressionNode(LiquidFilter filter, ExpressionNode expression) {
298      super();
299      this.filter = filter;
300      this.expression = expression;
301    }
302    
303  }
304  
305  private class LiquidStatement extends LiquidNode {
306    private String statement;
307    private List<LiquidExpressionNode> compiled = new ArrayList<>();
308
309    @Override
310    public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
311      if (compiled.size() == 0) {
312        FHIRLexer lexer = new FHIRLexer(statement, "liquid statement", false, true);
313        lexer.setLiquidMode(true);
314        compiled.add(new LiquidExpressionNode(null, engine.parse(lexer)));
315        while (!lexer.done()) {
316          if (lexer.getCurrent().equals("||")) {
317            lexer.next();
318            String f = lexer.getCurrent();
319            LiquidFilter filter = LiquidFilter.fromCode(f);
320            if (filter == null) {
321              lexer.error(engine.getWorker().formatMessage(I18nConstants.LIQUID_UNKNOWN_FILTER, f));
322            }
323            lexer.next();
324            if (!lexer.done() && lexer.getCurrent().equals(":")) {
325              lexer.next();
326              compiled.add(new LiquidExpressionNode(filter, engine.parse(lexer)));
327            } else {
328              compiled.add(new LiquidExpressionNode(filter, null));
329            }
330          } else {
331            lexer.error(engine.getWorker().formatMessage(I18nConstants.LIQUID_UNKNOWN_SYNTAX)); 
332          }
333        }
334      }
335      
336      String t = null;
337      for (LiquidExpressionNode i : compiled) {
338        if (i.filter == null) { // first
339          t = stmtToString(ctxt, engine.evaluate(ctxt, resource, resource, resource, i.expression));
340        } else switch (i.filter) {
341        case PREPEND:
342          t = stmtToString(ctxt, engine.evaluate(ctxt, resource, resource, resource, i.expression)) + t;
343          break; 
344        case MARKDOWNIFY:
345          t = processMarkdown(t);
346          break;
347        case UPCASE:
348          t = t.toUpperCase();
349          break;
350        case DOWNCASE:
351          t = t.toLowerCase();
352          break;
353        }
354      }
355      b.append(t);
356    }
357
358    private String processMarkdown(String t) {
359      return processor.process(t, "liquid");
360    }
361
362    private String stmtToString(LiquidEngineContext ctxt, List<Base> items) {
363      StringBuilder b = new StringBuilder();
364      boolean first = true;
365      for (Base i : items) {
366        if (i != null) {
367          if (first) first = false; else b.append(", ");
368          String s = renderingSupport != null ? renderingSupport.renderForLiquid(ctxt.externalContext, i) : null;
369          b.append(s != null ? s : engine.convertToString(i));
370        }
371      }
372      return b.toString();
373    }
374  }
375
376  private class LiquidElsIf extends LiquidNode {
377    private String condition;
378    private ExpressionNode compiled;
379    private List<LiquidNode> body = new ArrayList<>();
380
381    @Override
382    public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
383      for (LiquidNode n : body) {
384        n.evaluate(b, resource, ctxt);
385      }
386    }
387  }
388
389  private class LiquidIf extends LiquidNode {
390    private String condition;
391    private ExpressionNode compiled;
392    private List<LiquidNode> thenBody = new ArrayList<>();
393    private List<LiquidElsIf> elseIf = new ArrayList<>();
394    private List<LiquidNode> elseBody = new ArrayList<>();
395
396    @Override
397    public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
398      if (compiled == null)
399        compiled = engine.parse(condition);
400      boolean ok = engine.evaluateToBoolean(ctxt, resource, resource, resource, compiled);
401      List<LiquidNode> list = null;
402      if (ok) {
403        list = thenBody;
404
405      } else {
406        list = elseBody;
407        for (LiquidElsIf i : elseIf) {
408          if (i.compiled == null)
409            i.compiled = engine.parse(i.condition);
410          ok = engine.evaluateToBoolean(ctxt, resource, resource, resource, i.compiled);
411          if (ok) {
412            list = i.body;
413            break;
414          }
415        }
416      }
417      for (LiquidNode n : list) {
418        n.evaluate(b, resource, ctxt);
419      }
420    }
421  }
422
423  private class LiquidContinueExecuted extends FHIRException {
424    private static final long serialVersionUID = 4748737094188943721L;
425  }
426
427  private class LiquidContinue extends LiquidNode {
428    public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
429      throw new LiquidContinueExecuted();
430    }
431  }
432
433  private class LiquidBreakExecuted extends FHIRException {
434    private static final long serialVersionUID = 6328496371172871082L;
435  }
436
437  private class LiquidBreak extends LiquidNode {
438    public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
439      throw new LiquidBreakExecuted();
440    }
441  }
442
443  private class LiquidCycle extends LiquidNode {
444    private List<String> list = new ArrayList<>();
445    private int cursor = 0;
446
447    public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
448      b.append(list.get(cursor));
449      cursor++;
450      if (cursor == list.size()) {
451        cursor = 0;
452      }
453    }
454  }
455
456  private class LiquidAssign extends LiquidNode {
457    private String varName;
458    private String expression;
459    private ExpressionNode compiled;
460    @Override
461    public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
462      if (compiled == null) {
463        boolean dbl = engine.isAllowDoubleQuotes();
464        engine.setAllowDoubleQuotes(true);
465        ExpressionNodeWithOffset po = engine.parsePartial(expression, 0);
466        compiled = po.getNode();
467        engine.setAllowDoubleQuotes(dbl);
468      }
469      List<Base> list = engine.evaluate(ctxt, resource, resource, resource, compiled);
470      if (list.isEmpty()) {
471        ctxt.globalVars.remove(varName);
472      } else if (list.size() == 1) {
473        ctxt.globalVars.put(varName, list.get(0));
474      } else {
475        throw new Error("Assign returned a list?");
476      }      
477    }    
478  }
479  
480  private class LiquidFor extends LiquidNode {
481    private String varName;
482    private String condition;
483    private ExpressionNode compiled;
484    private boolean reversed = false;
485    private int limit = -1;
486    private int offset = -1;
487    private List<LiquidNode> body = new ArrayList<>();
488    private List<LiquidNode> elseBody = new ArrayList<>();
489
490    @Override
491    public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
492      if (compiled == null) {
493        ExpressionNodeWithOffset po = engine.parsePartial(condition, 0);
494        compiled = po.getNode();
495        if (po.getOffset() < condition.length()) {
496          parseModifiers(condition.substring(po.getOffset()));
497        }
498      }
499      List<Base> list = engine.evaluate(ctxt, resource, resource, resource, compiled);
500      LiquidEngineContext lctxt = new LiquidEngineContext(ctxt);
501      if (list.isEmpty()) {
502        for (LiquidNode n : elseBody) {
503          n.evaluate(b, resource, lctxt);
504        }
505      } else {
506        if (reversed) {
507          Collections.reverse(list);
508        }
509        int i = 0;
510        LiquidForLoopObject parentLoop = (LiquidForLoopObject) lctxt.globalVars.get("forLoop");
511        for (Base o : list) {
512          if (offset >= 0 && i < offset) {
513            i++;
514            continue;
515          }
516          if (limit >= 0 && i == limit) {
517            break;
518          }          
519          LiquidForLoopObject forloop = new LiquidForLoopObject(list.size(), i, offset, limit, parentLoop);
520          lctxt.globalVars.put("forLoop", forloop);
521          if (lctxt.globalVars.containsKey(varName)) {
522            throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_VARIABLE_ALREADY_ASSIGNED, varName));
523          }
524          lctxt.loopVars.put(varName, o);
525          boolean wantBreak = false;
526          for (LiquidNode n : body) {
527            try {
528              n.evaluate(b, resource, lctxt);
529            } catch (LiquidContinueExecuted e) {
530              break;
531            } catch (LiquidBreakExecuted e) {
532              wantBreak = true;
533              break;
534            }
535          }
536          if (wantBreak) {
537            break;
538          }
539          i++;
540        }
541        lctxt.globalVars.put("forLoop", parentLoop);
542      }
543    }
544
545    private void parseModifiers(String cnt) {
546      String src = cnt;
547      while (!Utilities.noString(cnt)) {
548        if (cnt.startsWith("reversed")) {
549          reversed = true;
550          cnt = cnt.substring(8);
551        } else if (cnt.startsWith("limit")) {
552          cnt = cnt.substring(5).trim();
553          if (!cnt.startsWith(":")) {
554            throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_COLON, src));
555          }
556          cnt = cnt.substring(1).trim();
557          int i = 0;
558          while (i < cnt.length() && Character.isDigit(cnt.charAt(i))) {
559            i++;
560          }
561          if (i == 0) {
562            throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_NUMBER, src));
563          }
564          limit = Integer.parseInt(cnt.substring(0, i));
565          cnt = cnt.substring(i);
566        } else if (cnt.startsWith("offset")) {
567          cnt = cnt.substring(6).trim();
568          if (!cnt.startsWith(":")) {
569            throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_COLON, src));
570          }
571          cnt = cnt.substring(1).trim();
572          int i = 0;
573          while (i < cnt.length() && Character.isDigit(cnt.charAt(i))) {
574            i++;
575          }
576          if (i == 0) {
577            throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_NUMBER, src));
578          }
579          offset = Integer.parseInt(cnt.substring(0, i));
580          cnt = cnt.substring(i);
581        } else {
582          throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_UNEXPECTED, cnt));
583        }
584      }      
585    }
586  }
587
588  private class LiquidCapture extends LiquidNode {
589    private String varName;
590    private List<LiquidNode> body = new ArrayList<>();
591
592    @Override
593    public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
594      StringBuilder bc = new StringBuilder();
595      for (LiquidNode n : body) {
596        n.evaluate(bc, resource, ctxt);
597      }
598      ctxt.globalVars.put(varName, new StringType(bc.toString()));
599    }
600  }
601
602  private class LiquidInclude extends LiquidNode {
603    private String page;
604    private Map<String, ExpressionNode> params = new HashMap<>();
605
606    @Override
607    public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
608      if (includeResolver == null) {
609        throw new FHIRException("Includes are not supported in this context");
610      }
611      String src = includeResolver.fetchInclude(LiquidEngine.this, page);
612      if (src == null) {
613        throw new FHIRException("The include '"+page+"' could not be resolved");
614      }
615      LiquidParser parser = new LiquidParser(src);
616      LiquidDocument doc = parser.parse(page);
617      LiquidEngineContext nctxt = new LiquidEngineContext(ctxt.externalContext, ctxt);
618      Tuple incl = new Tuple();
619      nctxt.loopVars.put("include", incl);
620      for (String s : params.keySet()) {
621        incl.addProperty(s, engine.evaluate(ctxt, resource, resource, resource, params.get(s)));
622      }
623      for (LiquidNode n : doc.body) {
624        n.evaluate(b, resource, nctxt);
625      }
626    }
627  }
628
629  public static class LiquidDocument {
630    private List<LiquidNode> body = new ArrayList<>();
631
632  }
633
634  private class LiquidParser {
635
636    private String source;
637    private int cursor;
638    private String name;
639
640    public LiquidParser(String source) {
641      this.source = source;
642      if (source == null) {
643        throw new FHIRException("No Liquid source to parse");
644      }
645      cursor = 0;
646    }
647
648    private char next1() {
649      if (cursor >= source.length())
650        return 0;
651      else
652        return source.charAt(cursor);
653    }
654
655    private char next2() {
656      if (cursor >= source.length() - 1)
657        return 0;
658      else
659        return source.charAt(cursor + 1);
660    }
661
662    private char grab() {
663      cursor++;
664      return source.charAt(cursor - 1);
665    }
666
667    public LiquidDocument parse(String name) throws FHIRException {
668      this.name = name;
669      LiquidDocument doc = new LiquidDocument();
670      parseList(doc.body, false, new String[0]);
671      return doc;
672    }
673
674    public LiquidCycle parseCycle(String cnt) {
675      LiquidCycle res = new LiquidCycle();
676      cnt = "," + cnt.substring(5).trim();
677      while (!Utilities.noString(cnt)) {
678        if (!cnt.startsWith(",")) {
679          throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_EXPECTING, name, cnt.charAt(0), ','));
680        }
681        cnt = cnt.substring(1).trim();
682        if (!cnt.startsWith("\"")) {
683          throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_EXPECTING, name, cnt.charAt(0), '"'));
684        }
685        cnt = cnt.substring(1);
686        int i = 0;
687        while (i < cnt.length() && cnt.charAt(i) != '"') {
688          i++;
689        }
690        if (i == cnt.length()) {
691          throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_UNTERMINATED, name));
692        }
693        res.list.add(cnt.substring(0, i));
694        cnt = cnt.substring(i + 1).trim();
695      }
696      return res;
697    }
698
699    private String parseList(List<LiquidNode> list, boolean inLoop, String[] terminators) throws FHIRException {
700      String close = null;
701      while (cursor < source.length()) {
702        if (next1() == '{' && (next2() == '%' || next2() == '{')) {
703          if (next2() == '%') {
704            String cnt = parseTag('%');
705            if (isTerminator(cnt, terminators)) {
706              close = cnt;
707              break;
708            } else if (cnt.startsWith("if "))
709              list.add(parseIf(cnt, inLoop));
710            else if (cnt.startsWith("loop ")) // loop is deprecated, but still
711                                              // supported
712              list.add(parseLoop(cnt.substring(4).trim()));
713            else if (cnt.startsWith("for "))
714              list.add(parseFor(cnt.substring(3).trim()));
715            else if (inLoop && cnt.equals("continue"))
716              list.add(new LiquidContinue());
717            else if (inLoop && cnt.equals("break"))
718              list.add(new LiquidBreak());
719            else if (inLoop && cnt.startsWith("cycle "))
720              list.add(parseCycle(cnt));
721            else if (cnt.startsWith("include "))
722              list.add(parseInclude(cnt.substring(7).trim()));
723            else if (cnt.startsWith("assign "))
724              list.add(parseAssign(cnt.substring(6).trim()));
725            else if (cnt.startsWith("capture "))
726              list.add(parseCapture(cnt.substring(7).trim()));
727            else
728              throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_UNKNOWN_FLOW_STMT,name, cnt));
729          } else { // next2() == '{'
730            list.add(parseStatement());
731          }
732        } else {
733          if (list.size() == 0 || !(list.get(list.size() - 1) instanceof LiquidConstant))
734            list.add(new LiquidConstant());
735          ((LiquidConstant) list.get(list.size() - 1)).addChar(grab());
736        }
737      }
738      for (LiquidNode n : list)
739        n.closeUp();
740      if (terminators.length > 0)
741        if (!isTerminator(close, terminators))
742          throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_UNKNOWN_NOEND, name, terminators));
743      return close;
744    }
745
746    private boolean isTerminator(String cnt, String[] terminators) {
747      if (Utilities.noString(cnt)) {
748        return false;
749      }
750      for (String t : terminators) {
751        if (t.endsWith(" ")) {
752          if (cnt.startsWith(t)) {
753            return true;
754          }
755        } else {
756          if (cnt.equals(t)) {
757            return true;
758          }
759        }
760      }
761      return false;
762    }
763
764    private LiquidNode parseIf(String cnt, boolean inLoop) throws FHIRException {
765      LiquidIf res = new LiquidIf();
766      res.condition = cnt.substring(3).trim();
767      String term = parseList(res.thenBody, inLoop, new String[] { "else", "elsif ", "endif" });
768      while (term.startsWith("elsif ")) {
769        LiquidElsIf elsIf = new LiquidElsIf();
770        res.elseIf.add(elsIf);
771        elsIf.condition = term.substring(5).trim();
772        term = parseList(elsIf.body, inLoop, new String[] { "elsif ", "else", "endif" });
773      }
774      if ("else".equals(term)) {
775        term = parseList(res.elseBody, inLoop, new String[] { "endif" });
776      }
777
778      return res;
779    }
780
781    private LiquidNode parseInclude(String cnt) throws FHIRException {
782      int i = 1;
783      while (i < cnt.length() && !Character.isWhitespace(cnt.charAt(i)))
784        i++;
785      if (i == 0)
786        throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_INCLUDE, name, cnt));
787      LiquidInclude res = new LiquidInclude();
788      res.page = cnt.substring(0, i);
789      while (i < cnt.length() && Character.isWhitespace(cnt.charAt(i)))
790        i++;
791      while (i < cnt.length()) {
792        int j = i;
793        while (i < cnt.length() && cnt.charAt(i) != '=')
794          i++;
795        if (i >= cnt.length() || j == i)
796          throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_INCLUDE, name,  cnt));
797        String n = cnt.substring(j, i);
798        if (res.params.containsKey(n))
799          throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_INCLUDE, name,  cnt));
800        i++;
801        ExpressionNodeWithOffset t = engine.parsePartial(cnt, i);
802        i = t.getOffset();
803        res.params.put(n, t.getNode());
804        while (i < cnt.length() && Character.isWhitespace(cnt.charAt(i)))
805          i++;
806      }
807      return res;
808    }
809
810    private LiquidNode parseLoop(String cnt) throws FHIRException {
811      int i = 0;
812      while (!Character.isWhitespace(cnt.charAt(i)))
813        i++;
814      LiquidFor res = new LiquidFor();
815      res.varName = cnt.substring(0, i);
816      if ("include".equals(res.varName)) {
817        throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_VARIABLE_ILLEGAL, res.varName));
818      }
819      while (Character.isWhitespace(cnt.charAt(i)))
820        i++;
821      int j = i;
822      while (!Character.isWhitespace(cnt.charAt(i)))
823        i++;
824      if (!"in".equals(cnt.substring(j, i)))
825        throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_LOOP, name, cnt));
826      res.condition = cnt.substring(i).trim();
827      parseList(res.body, false, new String[] { "endloop" });
828      return res;
829    }
830
831    private LiquidNode parseFor(String cnt) throws FHIRException {
832      int i = 0;
833      while (!Character.isWhitespace(cnt.charAt(i)))
834        i++;
835      LiquidFor res = new LiquidFor();
836      res.varName = cnt.substring(0, i);
837      if ("include".equals(res.varName)) {
838        throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_VARIABLE_ILLEGAL, res.varName));
839      }
840      while (Character.isWhitespace(cnt.charAt(i)))
841        i++;
842      int j = i;
843      while (!Character.isWhitespace(cnt.charAt(i)))
844        i++;
845      if (!"in".equals(cnt.substring(j, i)))
846        throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_LOOP, name, cnt));
847      res.condition = cnt.substring(i).trim();
848      String term = parseList(res.body, true, new String[] { "endfor", "else" });
849      if ("else".equals(term)) {
850        parseList(res.elseBody, false, new String[] { "endfor" });
851      }
852      return res;
853    }
854
855    private LiquidNode parseCapture(String cnt) throws FHIRException {
856      int i = 0;
857      while (i < cnt.length() && !Character.isWhitespace(cnt.charAt(i)))
858        i++;
859      LiquidCapture res = new LiquidCapture();
860      res.varName = cnt.substring(0, i);
861      parseList(res.body, true, new String[] { "endcapture" });
862      return res;
863    }
864
865    private LiquidNode parseAssign(String cnt) throws FHIRException {
866      int i = 0;
867      while (!Character.isWhitespace(cnt.charAt(i)))
868        i++;
869      LiquidAssign res = new LiquidAssign();
870      res.varName = cnt.substring(0, i);
871      while (Character.isWhitespace(cnt.charAt(i)))
872        i++;
873      int j = i;
874      while (!Character.isWhitespace(cnt.charAt(i)))
875        i++;
876      res.expression = cnt.substring(i).trim();
877      return res;
878    }
879
880    private String parseTag(char ch) throws FHIRException {
881      grab();
882      grab();
883      StringBuilder b = new StringBuilder();
884      while (cursor < source.length() && !(next1() == '%' && next2() == '}')) {
885        b.append(grab());
886      }
887      if (!(next1() == '%' && next2() == '}'))
888        throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_NOTERM, name,  "{% " + b.toString()));
889      grab();
890      grab();
891      return b.toString().trim();
892    }
893
894    private LiquidStatement parseStatement() throws FHIRException {
895      grab();
896      grab();
897      StringBuilder b = new StringBuilder();
898      while (cursor < source.length() && !(next1() == '}' && next2() == '}')) {
899        b.append(grab());
900      }
901      if (!(next1() == '}' && next2() == '}'))
902        throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_NOTERM, name,  "{{ " + b.toString()));
903      grab();
904      grab();
905      LiquidStatement res = new LiquidStatement();
906      res.statement = b.toString().trim();
907      return res;
908    }
909
910  }
911
912  @Override
913  public List<Base> resolveConstant(FHIRPathEngine engine, Object appContext, String name, boolean beforeContext, boolean explicitConstant) throws PathEngineException {
914    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
915    if (ctxt.loopVars.containsKey(name))
916      return new ArrayList<Base>(Arrays.asList(ctxt.loopVars.get(name)));
917    if (ctxt.globalVars.containsKey(name))
918      return new ArrayList<Base>(Arrays.asList(ctxt.globalVars.get(name)));
919    if (externalHostServices == null)
920      return new ArrayList<Base>();
921    return externalHostServices.resolveConstant(engine, ctxt.externalContext, name, beforeContext, explicitConstant);
922  }
923
924  @Override
925  public TypeDetails resolveConstantType(FHIRPathEngine engine, Object appContext, String name, boolean explicitConstant) throws PathEngineException {
926    if (externalHostServices == null)
927      return null;
928    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
929    return externalHostServices.resolveConstantType(engine, ctxt.externalContext, name, explicitConstant);
930  }
931
932  @Override
933  public boolean log(String argument, List<Base> focus) {
934    if (externalHostServices == null)
935      return false;
936    return externalHostServices.log(argument, focus);
937  }
938
939  @Override
940  public FunctionDetails resolveFunction(FHIRPathEngine engine, String functionName) {
941    // we define one function here: ensureId()
942    if (keyIssuer != null && "ensureId".equals(functionName)) {
943      return new FunctionDetails("Id for element, and make a unique one if it doens't exist", 0, 0);
944    }
945    if (externalHostServices == null)
946      return null;
947    return externalHostServices.resolveFunction(engine, functionName);
948  }
949
950  @Override
951  public TypeDetails checkFunction(FHIRPathEngine engine, Object appContext, String functionName, TypeDetails focus, List<TypeDetails> parameters) throws PathEngineException {
952    if (keyIssuer != null && "ensureId".equals(functionName)) {
953      return new TypeDetails(CollectionStatus.SINGLETON, "string");
954    }
955    if (externalHostServices == null)
956      return null;
957    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
958    return externalHostServices.checkFunction(engine, ctxt.externalContext, functionName, focus, parameters);
959  }
960
961  @Override
962  public List<Base> executeFunction(FHIRPathEngine engine, Object appContext, List<Base> focus, String functionName, List<List<Base>> parameters) {
963    if (keyIssuer != null && "ensureId".equals(functionName)) {
964      Base b = focus.get(0);
965      if (b.getIdBase() == null) {
966        b.setIdBase(keyIssuer.issueKey());
967      }
968      List<Base> res = new ArrayList<Base>();
969      res.add(new IdType(b.getIdBase()));
970      return res;
971    }
972    if (externalHostServices == null)
973      return null;
974    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
975    return externalHostServices.executeFunction(engine, ctxt.externalContext, focus, functionName, parameters);
976  }
977
978  @Override
979  public Base resolveReference(FHIRPathEngine engine, Object appContext, String url, Base refContext) throws FHIRException {
980    if (externalHostServices == null)
981      return null;
982    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
983    return resolveReference(engine, ctxt.externalContext, url, refContext);
984  }
985
986  @Override
987  public boolean conformsToProfile(FHIRPathEngine engine, Object appContext, Base item, String url) throws FHIRException {
988    if (externalHostServices == null)
989      return false;
990    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
991    return conformsToProfile(engine, ctxt.externalContext, item, url);
992  }
993
994  @Override
995  public ValueSet resolveValueSet(FHIRPathEngine engine, Object appContext, String url) {
996    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
997    if (externalHostServices != null)
998      return externalHostServices.resolveValueSet(engine, ctxt.externalContext, url);
999    else
1000      return engine.getWorker().fetchResource(ValueSet.class, url);
1001  }
1002
1003  /**
1004   * Lightweight method to replace fixed constants in resources
1005   * 
1006   * @param node
1007   * @param vars
1008   * @return
1009   */
1010  public boolean replaceInHtml(XhtmlNode node, Map<String, String> vars) {
1011    boolean replaced = false;
1012    if (node.getNodeType() == NodeType.Text || node.getNodeType() == NodeType.Comment) {
1013      String cnt = node.getContent();
1014      for (String n : vars.keySet()) {
1015        cnt = cnt.replace(n, vars.get(n));
1016      }
1017      if (!cnt.equals(node.getContent())) {
1018        node.setContent(cnt);
1019        replaced = true;
1020      }
1021    } else if (node.getNodeType() == NodeType.Element || node.getNodeType() == NodeType.Document) {
1022      for (XhtmlNode c : node.getChildNodes()) {
1023        if (replaceInHtml(c, vars)) {
1024          replaced = true;
1025        }
1026      }
1027      for (String an : node.getAttributes().keySet()) {
1028        String cnt = node.getAttributes().get(an);
1029        for (String n : vars.keySet()) {
1030          cnt = cnt.replace(n, vars.get(n));
1031        }
1032        if (!cnt.equals(node.getAttributes().get(an))) {
1033          node.getAttributes().put(an, cnt);
1034          replaced = true;
1035        }
1036      }
1037    }
1038    return replaced;
1039  }
1040
1041  @Override
1042  public boolean paramIsType(String name, int index) {
1043    return false;
1044  }
1045
1046  public FHIRPathEngine getEngine() {
1047    return engine;
1048  }
1049  
1050  
1051}