package ca.uhn.fhir.narrative.template;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.antlr.runtime.ANTLRFileStream;
import org.antlr.runtime.ANTLRStringStream;
import org.antlr.runtime.CommonTokenStream;
import org.antlr.runtime.RecognitionException;
import org.antlr.runtime.tree.CommonTree;
import org.antlr.runtime.tree.CommonTreeNodeStream;
import ca.uhn.fhir.narrative.template.filters.Filter;
import ca.uhn.fhir.narrative.template.nodes.LNode;
import ca.uhn.fhir.narrative.template.nodes.LiquidWalker;
import ca.uhn.fhir.narrative.template.parser.Flavor;
import ca.uhn.fhir.narrative.template.parser.LiquidLexer;
import ca.uhn.fhir.narrative.template.parser.LiquidParser;
import ca.uhn.fhir.narrative.template.tags.Tag;
/**
* The main class of this library. Use one of its static
* <code>parse(...)</code> to get a hold of a reference.
* <p/>
* Also see: https://github.com/Shopify/liquid
*/
public class Template {
/**
* The root of the AST denoting the Liquid input source.
*/
private final CommonTree root;
/**
* This instance's tags.
*/
private final Map<String, Tag> tags;
/**
* This instance's filters.
*/
private final Map<String, Filter> filters;
private final Flavor flavor;
/**
* Creates a new Template instance from a given input.
* @param input
* the file holding the Liquid source.
* @param tags
* the tags this instance will make use of.
* @param filters
* the filters this instance will make use of.
*/
private Template(String input, Map<String, Tag> tags, Map<String, Filter> filters) {
this(input, tags, filters, Flavor.LIQUID);
}
private Template(String input, Map<String, Tag> tags, Map<String, Filter> filters, Flavor flavor) {
this.tags = tags;
this.filters = filters;
this.flavor = flavor;
LiquidLexer lexer = new LiquidLexer(new ANTLRStringStream(input));
LiquidParser parser = new LiquidParser(flavor, new CommonTokenStream(lexer));
try {
root = (CommonTree) parser.parse().getTree();
}
catch (RecognitionException e) {
throw new RuntimeException("could not parse input: " + input, e);
}
}
/**
* Creates a new Template instance from a given file.
*
* @param file
* the file holding the Liquid source.
*/
private Template(File file, Map<String, Tag> tags, Map<String, Filter> filters) throws IOException {
this(file, tags, filters, Flavor.LIQUID);
}
private Template(File file, Map<String, Tag> tags, Map<String, Filter> filters, Flavor flavor) throws IOException {
this.tags = tags;
this.filters = filters;
this.flavor = flavor;
try {
LiquidLexer lexer = new LiquidLexer(new ANTLRFileStream(file.getAbsolutePath()));
LiquidParser parser = new LiquidParser(flavor, new CommonTokenStream(lexer));
root = (CommonTree) parser.parse().getTree();
}
catch (RecognitionException e) {
throw new RuntimeException("could not parse input from " + file, e);
}
}
/**
* Returns the root of the AST of the parsed input.
*
* @return the root of the AST of the parsed input.
*/
public CommonTree getAST() {
return root;
}
/**
* Returns a new Template instance from a given input string.
*
* @param input
* the input string holding the Liquid source.
*
* @return a new Template instance from a given input string.
*/
public static Template parse(String input) {
return new Template(input, Tag.getTags(), Filter.getFilters());
}
/**
* Returns a new Template instance from a given input file.
*
* @param file
* the input file holding the Liquid source.
*
* @return a new Template instance from a given input file.
*/
public static Template parse(File file) throws IOException {
return parse(file, Flavor.LIQUID);
}
public static Template parse(File file, Flavor flavor) throws IOException {
return new Template(file, Tag.getTags(), Filter.getFilters(), flavor);
}
public Template with(Tag tag) {
this.tags.put(tag.name, tag);
return this;
}
public Template with(Filter filter) {
this.filters.put(filter.name, filter);
return this;
}
/**
* Renders the template.
*
* @param context
* a Map denoting the (possibly nested)
* variables that can be used in this
* Template.
*
* @return a string denoting the rendered template.
*/
public String render(Map<String, Object> context) {
LiquidWalker walker = new LiquidWalker(new CommonTreeNodeStream(root), this.tags, this.filters, this.flavor);
try {
LNode node = walker.walk();
Object rendered = node.render(context);
return rendered == null ? "" : String.valueOf(rendered);
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Returns a string representation of the AST of the parsed
* input source.
*
* @return a string representation of the AST of the parsed
* input source.
*/
public String toStringAST() {
StringBuilder builder = new StringBuilder();
walk(root, builder);
return builder.toString();
}
/**
* Walks a (sub) tree of the root of the input source and builds
* a string representation of the structure of the AST.
* <p/>
* Note that line breaks and multiple white space characters are
* trimmed to a single white space character.
*
* @param tree
* the (sub) tree.
* @param builder
* the StringBuilder to fill.
*/
@SuppressWarnings("unchecked")
private void walk(CommonTree tree, StringBuilder builder) {
List<CommonTree> firstStack = new ArrayList<CommonTree>();
firstStack.add(tree);
List<List<CommonTree>> childListStack = new ArrayList<List<CommonTree>>();
childListStack.add(firstStack);
while (!childListStack.isEmpty()) {
List<CommonTree> childStack = childListStack.get(childListStack.size() - 1);
if (childStack.isEmpty()) {
childListStack.remove(childListStack.size() - 1);
}
else {
tree = childStack.remove(0);
String indent = "";
for (int i = 0; i < childListStack.size() - 1; i++) {
indent += (childListStack.get(i).size() > 0) ? "| " : " ";
}
String tokenName = LiquidParser.tokenNames[tree.getType()];
String tokenText = tree.getText().replaceAll("\\s+", " ").trim();
builder.append(indent)
.append(childStack.isEmpty() ? "'- " : "|- ")
.append(tokenName)
.append(!tokenName.equals(tokenText) ? "='" + tokenText + "'" : "")
.append("\n");
if (tree.getChildCount() > 0) {
childListStack.add(new ArrayList<CommonTree>((List<CommonTree>) tree.getChildren()));
}
}
}
}
}