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