Update Liquid Implementation

This commit is contained in:
Grahame Grieve 2020-05-23 08:15:37 +10:00
parent 50ea83322f
commit 3f4d574571
7 changed files with 338 additions and 97 deletions

View File

@ -61,7 +61,7 @@ public class LiquidRenderer extends ResourceRenderer {
XhtmlNode xn; XhtmlNode xn;
try { try {
LiquidDocument doc = engine.parse(liquidTemplate, "template"); LiquidDocument doc = engine.parse(liquidTemplate, "template");
String html = "rto do"; // engine.evaluate(doc, r, rcontext); String html = engine.evaluate(doc, r.getBase(), rcontext);
xn = new XhtmlParser().parseFragment(html); xn = new XhtmlParser().parseFragment(html);
if (!x.getName().equals("div")) if (!x.getName().equals("div"))
throw new FHIRException("Error in template: Root element is not 'div'"); throw new FHIRException("Error in template: Root element is not 'div'");

View File

@ -42,6 +42,7 @@ public class BaseWrappers {
public List<ResourceWrapper> getContained(); public List<ResourceWrapper> getContained();
public String getId(); public String getId();
public XhtmlNode getNarrative() throws FHIRFormatError, IOException, FHIRException; public XhtmlNode getNarrative() throws FHIRFormatError, IOException, FHIRException;
public Base getBase();
public String getName(); public String getName();
public void describe(XhtmlNode x) throws UnsupportedEncodingException, IOException; public void describe(XhtmlNode x) throws UnsupportedEncodingException, IOException;
public void injectNarrative(XhtmlNode x, NarrativeStatus status) throws IOException; public void injectNarrative(XhtmlNode x, NarrativeStatus status) throws IOException;

View File

@ -307,6 +307,11 @@ public class DOMWrappers {
public StructureDefinition getDefinition() { public StructureDefinition getDefinition() {
return definition; return definition;
} }
@Override
public Base getBase() {
throw new Error("Not Implemented yet");
}
} }
} }

View File

@ -214,6 +214,11 @@ public class DirectWrappers {
public StructureDefinition getDefinition() { public StructureDefinition getDefinition() {
return context.getWorker().fetchTypeDefinition(wrapped.fhirType()); return context.getWorker().fetchTypeDefinition(wrapped.fhirType());
} }
@Override
public Base getBase() {
return wrapped;
}
} }
} }

View File

@ -214,6 +214,11 @@ public class ElementWrappers {
public StructureDefinition getDefinition() { public StructureDefinition getDefinition() {
return definition; return definition;
} }
@Override
public Base getBase() {
return wrapped;
}
} }
public static class PropertyWrapperMetaElement extends RendererWrapperImpl implements PropertyWrapper { public static class PropertyWrapperMetaElement extends RendererWrapperImpl implements PropertyWrapper {

View File

@ -1,6 +1,7 @@
package org.hl7.fhir.r5.utils; package org.hl7.fhir.r5.utils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -34,7 +35,6 @@ import java.util.Map;
*/ */
import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.exceptions.PathEngineException; import org.hl7.fhir.exceptions.PathEngineException;
import org.hl7.fhir.r5.context.IWorkerContext; import org.hl7.fhir.r5.context.IWorkerContext;
@ -95,7 +95,7 @@ public class LiquidEngine implements IEvaluationContext {
return new LiquidParser(source).parse(sourceName); return new LiquidParser(source).parse(sourceName);
} }
public String evaluate(LiquidDocument document, Resource resource, Object appContext) throws FHIRException { public String evaluate(LiquidDocument document, Base resource, Object appContext) throws FHIRException {
StringBuilder b = new StringBuilder(); StringBuilder b = new StringBuilder();
LiquidEngineContext ctxt = new LiquidEngineContext(appContext); LiquidEngineContext ctxt = new LiquidEngineContext(appContext);
for (LiquidNode n : document.body) { for (LiquidNode n : document.body) {
@ -105,9 +105,10 @@ public class LiquidEngine implements IEvaluationContext {
} }
private abstract class LiquidNode { private abstract class LiquidNode {
protected void closeUp() {} protected void closeUp() {
}
public abstract void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException; public abstract void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException;
} }
private class LiquidConstant extends LiquidNode { private class LiquidConstant extends LiquidNode {
@ -125,7 +126,7 @@ public class LiquidEngine implements IEvaluationContext {
} }
@Override @Override
public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) { public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) {
b.append(constant); b.append(constant);
} }
} }
@ -135,47 +136,190 @@ public class LiquidEngine implements IEvaluationContext {
private ExpressionNode compiled; private ExpressionNode compiled;
@Override @Override
public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException { public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
if (compiled == null) if (compiled == null)
compiled = engine.parse(statement); compiled = engine.parse(statement);
b.append(engine.evaluateToString(ctxt, resource, resource, resource, compiled)); b.append(engine.evaluateToString(ctxt, resource, resource, resource, compiled));
} }
} }
private class LiquidElsIf extends LiquidNode {
private String condition;
private ExpressionNode compiled;
private List<LiquidNode> body = new ArrayList<>();
@Override
public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
for (LiquidNode n : body) {
n.evaluate(b, resource, ctxt);
}
}
}
private class LiquidIf extends LiquidNode { private class LiquidIf extends LiquidNode {
private String condition; private String condition;
private ExpressionNode compiled; private ExpressionNode compiled;
private List<LiquidNode> thenBody = new ArrayList<>(); private List<LiquidNode> thenBody = new ArrayList<>();
private List<LiquidElsIf> elseIf = new ArrayList<>();
private List<LiquidNode> elseBody = new ArrayList<>(); private List<LiquidNode> elseBody = new ArrayList<>();
@Override @Override
public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException { public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
if (compiled == null) if (compiled == null)
compiled = engine.parse(condition); compiled = engine.parse(condition);
boolean ok = engine.evaluateToBoolean(ctxt, resource, resource, resource, compiled); boolean ok = engine.evaluateToBoolean(ctxt, resource, resource, resource, compiled);
List<LiquidNode> list = ok ? thenBody : elseBody; List<LiquidNode> list = null;
if (ok) {
list = thenBody;
} else {
list = elseBody;
for (LiquidElsIf i : elseIf) {
if (i.compiled == null)
i.compiled = engine.parse(i.condition);
ok = engine.evaluateToBoolean(ctxt, resource, resource, resource, i.compiled);
if (ok) {
list = i.body;
break;
}
}
}
for (LiquidNode n : list) { for (LiquidNode n : list) {
n.evaluate(b, resource, ctxt); n.evaluate(b, resource, ctxt);
} }
} }
} }
private class LiquidLoop extends LiquidNode { private class LiquidContinueExecuted extends FHIRException {
private static final long serialVersionUID = 4748737094188943721L;
}
private class LiquidContinue extends LiquidNode {
public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
throw new LiquidContinueExecuted();
}
}
private class LiquidBreakExecuted extends FHIRException {
private static final long serialVersionUID = 6328496371172871082L;
}
private class LiquidBreak extends LiquidNode {
public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
throw new LiquidBreakExecuted();
}
}
private class LiquidCycle extends LiquidNode {
private List<String> list = new ArrayList<>();
private int cursor = 0;
public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
b.append(list.get(cursor));
cursor++;
if (cursor == list.size()) {
cursor = 0;
}
}
}
private class LiquidFor extends LiquidNode {
private String varName; private String varName;
private String condition; private String condition;
private ExpressionNode compiled; private ExpressionNode compiled;
private boolean reversed = false;
private int limit = -1;
private int offset = -1;
private List<LiquidNode> body = new ArrayList<>(); private List<LiquidNode> body = new ArrayList<>();
private List<LiquidNode> elseBody = new ArrayList<>();
@Override @Override
public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException { public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
if (compiled == null) if (compiled == null) {
compiled = engine.parse(condition); ExpressionNodeWithOffset po = engine.parsePartial(condition, 0);
compiled = po.getNode();
if (po.getOffset() < condition.length()) {
parseModifiers(condition.substring(po.getOffset()));
}
}
List<Base> list = engine.evaluate(ctxt, resource, resource, resource, compiled); List<Base> list = engine.evaluate(ctxt, resource, resource, resource, compiled);
LiquidEngineContext lctxt = new LiquidEngineContext(ctxt); LiquidEngineContext lctxt = new LiquidEngineContext(ctxt);
for (Base o : list) { if (list.isEmpty()) {
lctxt.vars.put(varName, o); for (LiquidNode n : elseBody) {
for (LiquidNode n : body) {
n.evaluate(b, resource, lctxt); n.evaluate(b, resource, lctxt);
} }
} else {
if (reversed) {
Collections.reverse(list);
}
int i = 0;
for (Base o : list) {
if (offset >= 0 && i < offset) {
i++;
continue;
}
if (limit >= 0 && i == limit) {
break;
}
lctxt.vars.put(varName, o);
boolean wantBreak = false;
for (LiquidNode n : body) {
try {
n.evaluate(b, resource, lctxt);
} catch (LiquidContinueExecuted e) {
break;
} catch (LiquidBreakExecuted e) {
wantBreak = true;
break;
}
}
if (wantBreak) {
break;
}
i++;
}
}
}
private void parseModifiers(String cnt) {
String src = cnt;
while (!Utilities.noString(cnt)) {
if (cnt.startsWith("reversed")) {
reversed = true;
cnt = cnt.substring(8);
} else if (cnt.startsWith("limit")) {
cnt = cnt.substring(5).trim();
if (!cnt.startsWith(":")) {
throw new FHIRException("Exception evaluating "+src+": limit is not followed by ':'");
}
cnt = cnt.substring(1).trim();
int i = 0;
while (i < cnt.length() && Character.isDigit(cnt.charAt(i))) {
i++;
}
if (i == 0) {
throw new FHIRException("Exception evaluating "+src+": limit is not followed by a number");
}
limit = Integer.parseInt(cnt.substring(0, i));
cnt = cnt.substring(i);
} else if (cnt.startsWith("offset")) {
cnt = cnt.substring(6).trim();
if (!cnt.startsWith(":")) {
throw new FHIRException("Exception evaluating "+src+": limit is not followed by ':'");
}
cnt = cnt.substring(1).trim();
int i = 0;
while (i < cnt.length() && Character.isDigit(cnt.charAt(i))) {
i++;
}
if (i == 0) {
throw new FHIRException("Exception evaluating "+src+": limit is not followed by a number");
}
offset = Integer.parseInt(cnt.substring(0, i));
cnt = cnt.substring(i);
} else {
throw new FHIRException("Exception evaluating "+src+": unexpected content at "+cnt);
}
} }
} }
} }
@ -185,7 +329,7 @@ public class LiquidEngine implements IEvaluationContext {
private Map<String, ExpressionNode> params = new HashMap<>(); private Map<String, ExpressionNode> params = new HashMap<>();
@Override @Override
public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException { public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
String src = includeResolver.fetchInclude(LiquidEngine.this, page); String src = includeResolver.fetchInclude(LiquidEngine.this, page);
LiquidParser parser = new LiquidParser(src); LiquidParser parser = new LiquidParser(src);
LiquidDocument doc = parser.parse(page); LiquidDocument doc = parser.parse(page);
@ -225,64 +369,124 @@ public class LiquidEngine implements IEvaluationContext {
} }
private char next2() { private char next2() {
if (cursor >= source.length()-1) if (cursor >= source.length() - 1)
return 0; return 0;
else else
return source.charAt(cursor+1); return source.charAt(cursor + 1);
} }
private char grab() { private char grab() {
cursor++; cursor++;
return source.charAt(cursor-1); return source.charAt(cursor - 1);
} }
public LiquidDocument parse(String name) throws FHIRException { public LiquidDocument parse(String name) throws FHIRException {
this.name = name; this.name = name;
LiquidDocument doc = new LiquidDocument(); LiquidDocument doc = new LiquidDocument();
parseList(doc.body, new String[0]); parseList(doc.body, false, new String[0]);
return doc; return doc;
} }
private String parseList(List<LiquidNode> list, String[] terminators) throws FHIRException { public LiquidCycle parseCycle(String cnt) {
LiquidCycle res = new LiquidCycle();
cnt = "," + cnt.substring(5).trim();
while (!Utilities.noString(cnt)) {
if (!cnt.startsWith(",")) {
throw new FHIRException("Script " + name + ": Script " + name + ": Found " + cnt.charAt(0) + " expecting ',' parsing cycle");
}
cnt = cnt.substring(1).trim();
if (!cnt.startsWith("\"")) {
throw new FHIRException("Script " + name + ": Script " + name + ": Found " + cnt.charAt(0) + " expecting '\"' parsing cycle");
}
cnt = cnt.substring(1);
int i = 0;
while (i < cnt.length() && cnt.charAt(i) != '"') {
i++;
}
if (i == cnt.length()) {
throw new FHIRException("Script " + name + ": Script " + name + ": Found unterminated string parsing cycle");
}
res.list.add(cnt.substring(0, i));
cnt = cnt.substring(i + 1).trim();
}
return res;
}
private String parseList(List<LiquidNode> list, boolean inLoop, String[] terminators) throws FHIRException {
String close = null; String close = null;
while (cursor < source.length()) { while (cursor < source.length()) {
if (next1() == '{' && (next2() == '%' || next2() == '{' )) { if (next1() == '{' && (next2() == '%' || next2() == '{')) {
if (next2() == '%') { if (next2() == '%') {
String cnt = parseTag('%'); String cnt = parseTag('%');
if (Utilities.existsInList(cnt, terminators)) { if (isTerminator(cnt, terminators)) {
close = cnt; close = cnt;
break; break;
} else if (cnt.startsWith("if ")) } else if (cnt.startsWith("if "))
list.add(parseIf(cnt)); list.add(parseIf(cnt, inLoop));
else if (cnt.startsWith("loop ")) else if (cnt.startsWith("loop ")) // loop is deprecated, but still
// supported
list.add(parseLoop(cnt.substring(4).trim())); list.add(parseLoop(cnt.substring(4).trim()));
else if (cnt.startsWith("for "))
list.add(parseFor(cnt.substring(3).trim()));
else if (inLoop && cnt.equals("continue"))
list.add(new LiquidContinue());
else if (inLoop && cnt.equals("break"))
list.add(new LiquidBreak());
else if (inLoop && cnt.startsWith("cycle "))
list.add(parseCycle(cnt));
else if (cnt.startsWith("include ")) else if (cnt.startsWith("include "))
list.add(parseInclude(cnt.substring(7).trim())); list.add(parseInclude(cnt.substring(7).trim()));
else else
throw new FHIRException("Script "+name+": Script "+name+": Unknown flow control statement "+cnt); throw new FHIRException("Script " + name + ": Script " + name + ": Unknown flow control statement " + cnt);
} else { // next2() == '{' } else { // next2() == '{'
list.add(parseStatement()); list.add(parseStatement());
} }
} else { } else {
if (list.size() == 0 || !(list.get(list.size()-1) instanceof LiquidConstant)) if (list.size() == 0 || !(list.get(list.size() - 1) instanceof LiquidConstant))
list.add(new LiquidConstant()); list.add(new LiquidConstant());
((LiquidConstant) list.get(list.size()-1)).addChar(grab()); ((LiquidConstant) list.get(list.size() - 1)).addChar(grab());
} }
} }
for (LiquidNode n : list) for (LiquidNode n : list)
n.closeUp(); n.closeUp();
if (terminators.length > 0) if (terminators.length > 0)
if (!Utilities.existsInList(close, terminators)) if (!isTerminator(close, terminators))
throw new FHIRException("Script "+name+": Script "+name+": Found end of script looking for "+terminators); throw new FHIRException("Script " + name + ": Script " + name + ": Found end of script looking for " + terminators);
return close; return close;
} }
private LiquidNode parseIf(String cnt) throws FHIRException { private boolean isTerminator(String cnt, String[] terminators) {
if (Utilities.noString(cnt)) {
return false;
}
for (String t : terminators) {
if (t.endsWith(" ")) {
if (cnt.startsWith(t)) {
return true;
}
} else {
if (cnt.equals(t)) {
return true;
}
}
}
return false;
}
private LiquidNode parseIf(String cnt, boolean inLoop) throws FHIRException {
LiquidIf res = new LiquidIf(); LiquidIf res = new LiquidIf();
res.condition = cnt.substring(3).trim(); res.condition = cnt.substring(3).trim();
String term = parseList(res.thenBody, new String[] { "else", "endif"} ); String term = parseList(res.thenBody, inLoop, new String[] { "else", "elsif ", "endif" });
if ("else".equals(term)) while (term.startsWith("elsif ")) {
term = parseList(res.elseBody, new String[] { "endif"} ); LiquidElsIf elsIf = new LiquidElsIf();
res.elseIf.add(elsIf);
elsIf.condition = term.substring(5).trim();
term = parseList(elsIf.body, inLoop, new String[] { "elsif ", "else", "endif" });
}
if ("else".equals(term)) {
term = parseList(res.elseBody, inLoop, new String[] { "endif" });
}
return res; return res;
} }
@ -291,7 +495,7 @@ public class LiquidEngine implements IEvaluationContext {
while (i < cnt.length() && !Character.isWhitespace(cnt.charAt(i))) while (i < cnt.length() && !Character.isWhitespace(cnt.charAt(i)))
i++; i++;
if (i == cnt.length() || i == 0) if (i == cnt.length() || i == 0)
throw new FHIRException("Script "+name+": Error reading include: "+cnt); throw new FHIRException("Script " + name + ": Error reading include: " + cnt);
LiquidInclude res = new LiquidInclude(); LiquidInclude res = new LiquidInclude();
res.page = cnt.substring(0, i); res.page = cnt.substring(0, i);
while (i < cnt.length() && Character.isWhitespace(cnt.charAt(i))) while (i < cnt.length() && Character.isWhitespace(cnt.charAt(i)))
@ -301,10 +505,10 @@ public class LiquidEngine implements IEvaluationContext {
while (i < cnt.length() && cnt.charAt(i) != '=') while (i < cnt.length() && cnt.charAt(i) != '=')
i++; i++;
if (i >= cnt.length() || j == i) if (i >= cnt.length() || j == i)
throw new FHIRException("Script "+name+": Error reading include: "+cnt); throw new FHIRException("Script " + name + ": Error reading include: " + cnt);
String n = cnt.substring(j, i); String n = cnt.substring(j, i);
if (res.params.containsKey(n)) if (res.params.containsKey(n))
throw new FHIRException("Script "+name+": Error reading include: "+cnt); throw new FHIRException("Script " + name + ": Error reading include: " + cnt);
i++; i++;
ExpressionNodeWithOffset t = engine.parsePartial(cnt, i); ExpressionNodeWithOffset t = engine.parsePartial(cnt, i);
i = t.getOffset(); i = t.getOffset();
@ -315,12 +519,11 @@ public class LiquidEngine implements IEvaluationContext {
return res; return res;
} }
private LiquidNode parseLoop(String cnt) throws FHIRException { private LiquidNode parseLoop(String cnt) throws FHIRException {
int i = 0; int i = 0;
while (!Character.isWhitespace(cnt.charAt(i))) while (!Character.isWhitespace(cnt.charAt(i)))
i++; i++;
LiquidLoop res = new LiquidLoop(); LiquidFor res = new LiquidFor();
res.varName = cnt.substring(0, i); res.varName = cnt.substring(0, i);
while (Character.isWhitespace(cnt.charAt(i))) while (Character.isWhitespace(cnt.charAt(i)))
i++; i++;
@ -328,12 +531,34 @@ public class LiquidEngine implements IEvaluationContext {
while (!Character.isWhitespace(cnt.charAt(i))) while (!Character.isWhitespace(cnt.charAt(i)))
i++; i++;
if (!"in".equals(cnt.substring(j, i))) if (!"in".equals(cnt.substring(j, i)))
throw new FHIRException("Script "+name+": Script "+name+": Error reading loop: "+cnt); throw new FHIRException("Script " + name + ": Script " + name + ": Error reading loop: " + cnt);
res.condition = cnt.substring(i).trim(); res.condition = cnt.substring(i).trim();
parseList(res.body, new String[] { "endloop"} ); parseList(res.body, false, new String[] { "endloop" });
return res; return res;
} }
private LiquidNode parseFor(String cnt) throws FHIRException {
int i = 0;
while (!Character.isWhitespace(cnt.charAt(i)))
i++;
LiquidFor res = new LiquidFor();
res.varName = cnt.substring(0, i);
while (Character.isWhitespace(cnt.charAt(i)))
i++;
int j = i;
while (!Character.isWhitespace(cnt.charAt(i)))
i++;
if (!"in".equals(cnt.substring(j, i)))
throw new FHIRException("Script " + name + ": Script " + name + ": Error reading loop: " + cnt);
res.condition = cnt.substring(i).trim();
String term = parseList(res.body, true, new String[] { "endfor", "else" });
if ("else".equals(term)) {
parseList(res.elseBody, false, new String[] { "endfor" });
}
return res;
}
private String parseTag(char ch) throws FHIRException { private String parseTag(char ch) throws FHIRException {
grab(); grab();
grab(); grab();
@ -342,7 +567,7 @@ public class LiquidEngine implements IEvaluationContext {
b.append(grab()); b.append(grab());
} }
if (!(next1() == '%' && next2() == '}')) if (!(next1() == '%' && next2() == '}'))
throw new FHIRException("Script "+name+": Unterminated Liquid statement {% "+b.toString()); throw new FHIRException("Script " + name + ": Unterminated Liquid statement {% " + b.toString());
grab(); grab();
grab(); grab();
return b.toString().trim(); return b.toString().trim();
@ -356,7 +581,7 @@ public class LiquidEngine implements IEvaluationContext {
b.append(grab()); b.append(grab());
} }
if (!(next1() == '}' && next2() == '}')) if (!(next1() == '}' && next2() == '}'))
throw new FHIRException("Script "+name+": Unterminated Liquid statement {{ "+b.toString()); throw new FHIRException("Script " + name + ": Unterminated Liquid statement {{ " + b.toString());
grab(); grab();
grab(); grab();
LiquidStatement res = new LiquidStatement(); LiquidStatement res = new LiquidStatement();

View File

@ -57,7 +57,7 @@ public class LiquidEngineTests implements ILiquidEngineIcludeResolver {
this.test = test; this.test = test;
LiquidDocument doc = engine.parse(test.get("template").getAsString(), "test-script"); LiquidDocument doc = engine.parse(test.get("template").getAsString(), "test-script");
String output = engine.evaluate(doc, loadResource(), null); String output = engine.evaluate(doc, loadResource(), null);
Assertions.assertTrue(test.get("output").getAsString().equals(output)); Assertions.assertEquals(test.get("output").getAsString(), output);
} }
@Override @Override