From 20b60c8d84849b4b0c38fb9382bee28b8948565a Mon Sep 17 00:00:00 2001 From: James Date: Sun, 25 Sep 2016 17:54:44 -0400 Subject: [PATCH] Initial commit --- hapi-fhir-narrativegenerator/.classpath | 33 ++ hapi-fhir-narrativegenerator/.project | 31 + .../org.eclipse.core.resources.prefs | 2 + .../.settings/org.eclipse.m2e.core.prefs | 4 + hapi-fhir-narrativegenerator/pom.xml | 84 +++ .../narrative/template/nodes/LiquidWalker.g | 274 +++++++++ .../fhir/narrative/template/parser/Liquid.g | 545 ++++++++++++++++++ .../uhn/fhir/narrative/template/LValue.java | 248 ++++++++ .../uhn/fhir/narrative/template/Template.java | 248 ++++++++ .../template/TemplateNarrativeGenerator.java | 101 ++++ .../narrative/template/filters/Append.java | 15 + .../template/filters/Capitalize.java | 23 + .../fhir/narrative/template/filters/Date.java | 255 ++++++++ .../template/filters/Divided_By.java | 31 + .../narrative/template/filters/Downcase.java | 15 + .../narrative/template/filters/Escape.java | 20 + .../template/filters/Escape_Once.java | 21 + .../narrative/template/filters/Filter.java | 169 ++++++ .../narrative/template/filters/First.java | 17 + .../fhir/narrative/template/filters/H.java | 15 + .../fhir/narrative/template/filters/Join.java | 35 ++ .../fhir/narrative/template/filters/Last.java | 17 + .../fhir/narrative/template/filters/Map.java | 39 ++ .../narrative/template/filters/Minus.java | 27 + .../narrative/template/filters/Modulo.java | 27 + .../template/filters/Newline_To_Br.java | 15 + .../fhir/narrative/template/filters/Plus.java | 27 + .../narrative/template/filters/Prepend.java | 15 + .../narrative/template/filters/Remove.java | 23 + .../template/filters/Remove_First.java | 25 + .../narrative/template/filters/Replace.java | 35 ++ .../template/filters/Replace_First.java | 39 ++ .../fhir/narrative/template/filters/Size.java | 30 + .../fhir/narrative/template/filters/Sort.java | 87 +++ .../narrative/template/filters/Split.java | 23 + .../template/filters/Strip_HTML.java | 18 + .../template/filters/Strip_Newlines.java | 15 + .../narrative/template/filters/Times.java | 27 + .../narrative/template/filters/Truncate.java | 45 ++ .../template/filters/Truncatewords.java | 47 ++ .../narrative/template/filters/Upcase.java | 15 + .../narrative/template/nodes/AndNode.java | 26 + .../narrative/template/nodes/AtomNode.java | 24 + .../template/nodes/AttributeNode.java | 23 + .../narrative/template/nodes/BlockNode.java | 58 ++ .../template/nodes/ContainsNode.java | 34 ++ .../fhir/narrative/template/nodes/EqNode.java | 26 + .../narrative/template/nodes/FilterNode.java | 41 ++ .../narrative/template/nodes/GtEqNode.java | 26 + .../fhir/narrative/template/nodes/GtNode.java | 26 + .../fhir/narrative/template/nodes/LNode.java | 21 + .../narrative/template/nodes/LookupNode.java | 73 +++ .../narrative/template/nodes/LtEqNode.java | 26 + .../fhir/narrative/template/nodes/LtNode.java | 26 + .../narrative/template/nodes/NEqNode.java | 26 + .../fhir/narrative/template/nodes/OrNode.java | 26 + .../narrative/template/nodes/OutputNode.java | 32 + .../narrative/template/nodes/TagNode.java | 40 ++ .../narrative/template/parser/Flavor.java | 13 + .../fhir/narrative/template/tags/Assign.java | 39 ++ .../fhir/narrative/template/tags/Capture.java | 23 + .../fhir/narrative/template/tags/Case.java | 60 ++ .../fhir/narrative/template/tags/Comment.java | 16 + .../fhir/narrative/template/tags/Cycle.java | 84 +++ .../uhn/fhir/narrative/template/tags/For.java | 253 ++++++++ .../uhn/fhir/narrative/template/tags/If.java | 27 + .../fhir/narrative/template/tags/Include.java | 43 ++ .../uhn/fhir/narrative/template/tags/Raw.java | 16 + .../narrative/template/tags/Tablerow.java | 126 ++++ .../uhn/fhir/narrative/template/tags/Tag.java | 120 ++++ .../fhir/narrative/template/tags/Unless.java | 27 + .../TemplateNarrativeGeneratorTest.java | 32 + .../src/test/resources/patient.narrative | 8 + pom.xml | 9 + 74 files changed, 4232 insertions(+) create mode 100644 hapi-fhir-narrativegenerator/.classpath create mode 100644 hapi-fhir-narrativegenerator/.project create mode 100644 hapi-fhir-narrativegenerator/.settings/org.eclipse.core.resources.prefs create mode 100644 hapi-fhir-narrativegenerator/.settings/org.eclipse.m2e.core.prefs create mode 100644 hapi-fhir-narrativegenerator/pom.xml create mode 100644 hapi-fhir-narrativegenerator/src/main/antlr3/ca/uhn/fhir/narrative/template/nodes/LiquidWalker.g create mode 100644 hapi-fhir-narrativegenerator/src/main/antlr3/ca/uhn/fhir/narrative/template/parser/Liquid.g create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/LValue.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/Template.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/TemplateNarrativeGenerator.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Append.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Capitalize.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Date.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Divided_By.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Downcase.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Escape.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Escape_Once.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Filter.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/First.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/H.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Join.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Last.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Map.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Minus.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Modulo.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Newline_To_Br.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Plus.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Prepend.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Remove.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Remove_First.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Replace.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Replace_First.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Size.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Sort.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Split.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Strip_HTML.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Strip_Newlines.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Times.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Truncate.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Truncatewords.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Upcase.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/AndNode.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/AtomNode.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/AttributeNode.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/BlockNode.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/ContainsNode.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/EqNode.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/FilterNode.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/GtEqNode.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/GtNode.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/LNode.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/LookupNode.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/LtEqNode.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/LtNode.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/NEqNode.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/OrNode.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/OutputNode.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/TagNode.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/parser/Flavor.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Assign.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Capture.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Case.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Comment.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Cycle.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/For.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/If.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Include.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Raw.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Tablerow.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Tag.java create mode 100644 hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Unless.java create mode 100644 hapi-fhir-narrativegenerator/src/test/java/ca/uhn/fhir/narrative/template/TemplateNarrativeGeneratorTest.java create mode 100644 hapi-fhir-narrativegenerator/src/test/resources/patient.narrative diff --git a/hapi-fhir-narrativegenerator/.classpath b/hapi-fhir-narrativegenerator/.classpath new file mode 100644 index 00000000000..3314705f3ff --- /dev/null +++ b/hapi-fhir-narrativegenerator/.classpath @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hapi-fhir-narrativegenerator/.project b/hapi-fhir-narrativegenerator/.project new file mode 100644 index 00000000000..579a2b6f9ac --- /dev/null +++ b/hapi-fhir-narrativegenerator/.project @@ -0,0 +1,31 @@ + + + hapi-fhir-narrativegenerator + NO_M2ECLIPSE_SUPPORT: Project files created with the maven-eclipse-plugin are not supported in M2Eclipse. + + + + + org.eclipse.wst.common.project.facet.core.builder + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jem.workbench.JavaEMFNature + org.eclipse.wst.common.modulecore.ModuleCoreNature + org.eclipse.m2e.core.maven2Nature + org.eclipse.jdt.core.javanature + org.eclipse.wst.common.project.facet.core.nature + + diff --git a/hapi-fhir-narrativegenerator/.settings/org.eclipse.core.resources.prefs b/hapi-fhir-narrativegenerator/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000000..99f26c0203a --- /dev/null +++ b/hapi-fhir-narrativegenerator/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/hapi-fhir-narrativegenerator/.settings/org.eclipse.m2e.core.prefs b/hapi-fhir-narrativegenerator/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000000..f897a7f1cb2 --- /dev/null +++ b/hapi-fhir-narrativegenerator/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/hapi-fhir-narrativegenerator/pom.xml b/hapi-fhir-narrativegenerator/pom.xml new file mode 100644 index 00000000000..62752b48420 --- /dev/null +++ b/hapi-fhir-narrativegenerator/pom.xml @@ -0,0 +1,84 @@ + + + 4.0.0 + + ca.uhn.hapi.fhir + hapi-deployable-pom + 2.1-SNAPSHOT + ../hapi-deployable-pom/pom.xml + + + hapi-fhir-hapi-fhir-narrativegenerator + jar + + HAPI FHIR - Narrative Generator + + + + + ca.uhn.hapi.fhir + hapi-fhir-base + 2.1-SNAPSHOT + + + + ca.uhn.hapi.fhir + hapi-fhir-structures-dstu2 + 2.1-SNAPSHOT + + + ca.uhn.hapi.fhir + hapi-fhir-structures-dstu3 + 2.1-SNAPSHOT + + + + org.antlr + antlr + 3.5.1 + + + + + + + + org.antlr + antlr3-maven-plugin + 3.5.2 + + + + antlr + + + + + + org.jacoco + jacoco-maven-plugin + + + ${basedir}/target/classes + ${basedir}/../hapi-fhir-base/target/classes + + + ${basedir}/src/main/java + ${basedir}/../hapi-fhir-base/src/main/java + + true + + + + default-prepare-agent + + prepare-agent + + + + + + + + diff --git a/hapi-fhir-narrativegenerator/src/main/antlr3/ca/uhn/fhir/narrative/template/nodes/LiquidWalker.g b/hapi-fhir-narrativegenerator/src/main/antlr3/ca/uhn/fhir/narrative/template/nodes/LiquidWalker.g new file mode 100644 index 00000000000..1e0794272a4 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/antlr3/ca/uhn/fhir/narrative/template/nodes/LiquidWalker.g @@ -0,0 +1,274 @@ +/* + * Copyright (c) 2010 by Bart Kiers + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * Project : Liqp; a Liquid Template grammar/parser + * Developed by : Bart Kiers, bart@big-o.nl + */ +tree grammar LiquidWalker; + +options { + tokenVocab=Liquid; + ASTLabelType=CommonTree; +} + +@header { + package ca.uhn.fhir.narrative.template.nodes; + + import ca.uhn.fhir.narrative.template.parser.*; + import ca.uhn.fhir.narrative.template.tags.*; + import ca.uhn.fhir.narrative.template.filters.*; + + import java.util.Map; +} + +@members { + + private Map tags; + private Map filters; + private Flavor flavor; + + public LiquidWalker(TreeNodeStream nodes, Map tags, Map filters) { + this(nodes, tags, filters, Flavor.LIQUID); + } + + public LiquidWalker(TreeNodeStream nodes, Map tags, Map filters, Flavor flavor) { + super(nodes); + this.tags = tags; + this.filters = filters; + this.flavor = flavor; + } +} + +walk returns [LNode node] + : block {$node = $block.node;} + ; + +block returns [BlockNode node] +@init{$node = new BlockNode();} + : ^(BLOCK (atom {$node.add($atom.node);})*) + ; + +atom returns [LNode node] + : tag {$node = $tag.node;} + | output {$node = $output.node;} + | assignment {$node = $assignment.node;} + | PLAIN {$node = new AtomNode($PLAIN.text);} + ; + +tag returns [LNode node] + : raw_tag {$node = $raw_tag.node;} + | comment_tag {$node = $comment_tag.node;} + | if_tag {$node = $if_tag.node;} + | unless_tag {$node = $unless_tag.node;} + | case_tag {$node = $case_tag.node;} + | cycle_tag {$node = $cycle_tag.node;} + | for_tag {$node = $for_tag.node;} + | table_tag {$node = $table_tag.node;} + | capture_tag {$node = $capture_tag.node;} + | include_tag {$node = $include_tag.node;} + | custom_tag {$node = $custom_tag.node;} + | custom_tag_block {$node = $custom_tag_block.node;} + | break_tag {$node = $break_tag.node;} + | continue_tag {$node = $continue_tag.node;} + ; + +raw_tag returns [LNode node] + : RAW {$node = new TagNode("raw", tags.get("raw"), new AtomNode($RAW.text));} + ; + +comment_tag returns [LNode node] + : COMMENT {$node = new TagNode("comment", tags.get("comment"), new AtomNode($COMMENT.text));} + ; + +if_tag returns [LNode node] +@init{List nodes = new ArrayList();} + : ^(IF e1=expr b1=block {nodes.add($e1.node); nodes.add($b1.node);} + (^(ELSIF e2=expr b2=block {nodes.add($e2.node); nodes.add($b2.node);} ))* + ^(ELSE (b3=block {nodes.add(new AtomNode("TRUE")); nodes.add($b3.node);} )?) + ) + {$node = new TagNode("if", tags.get("if"), nodes.toArray(new LNode[nodes.size()]));} + ; + +unless_tag returns [LNode node] +@init{List nodes = new ArrayList();} + : ^(UNLESS expr b1=block {nodes.add($expr.node); nodes.add($b1.node);} + ^(ELSE (b2=block {nodes.add(new AtomNode(null)); nodes.add($b2.node);})?)) + {$node = new TagNode("unless", tags.get("unless"), nodes.toArray(new LNode[nodes.size()]));} + ; + +case_tag returns [LNode node] +@init{List nodes = new ArrayList();} + : ^(CASE expr {nodes.add($expr.node);} + (when_tag [nodes] )+ + ^(ELSE (block {nodes.add(nodes.get(0)); nodes.add($block.node);} )?)) + {$node = new TagNode("case", tags.get("case"), nodes.toArray(new LNode[nodes.size()]));} + ; + +when_tag[List nodes] + : ^(WHEN (expr {nodes.add($expr.node);})+ block) {nodes.add($block.node);} + ; + +cycle_tag returns [LNode node] +@init{List nodes = new ArrayList();} + : ^(CYCLE cycle_group {nodes.add($cycle_group.node);} (e=expr {nodes.add($e.node);})+) + {$node = new TagNode("cycle", tags.get("cycle"), nodes.toArray(new LNode[nodes.size()]));} + ; + +cycle_group returns [LNode node] + : ^(GROUP expr?) {$node = $expr.node;} + ; + +for_tag returns [LNode node] + : for_array {$node = $for_array.node;} + | for_range {$node = $for_range.node;} + ; + +for_array returns [LNode node] +@init{ + List expressions = new ArrayList(); + expressions.add(new AtomNode(true)); +} + : ^(FOR_ARRAY Id lookup {expressions.add(new AtomNode($Id.text)); expressions.add($lookup.node);} + for_block {expressions.add($for_block.node1); expressions.add($for_block.node2);} + ^(ATTRIBUTES (attribute {expressions.add($attribute.node);})*) + ) + {$node = new TagNode("for", tags.get("for"), expressions.toArray(new LNode[expressions.size()]));} + ; + +for_range returns [LNode node] +@init{ + List expressions = new ArrayList(); + expressions.add(new AtomNode(false)); +} + : ^(FOR_RANGE Id from=expr to=expr {expressions.add(new AtomNode($Id.text)); expressions.add($from.node); expressions.add($to.node);} + block {expressions.add($block.node);} + ^(ATTRIBUTES (attribute {expressions.add($attribute.node);})*) + ) + {$node = new TagNode("for", tags.get("for"), expressions.toArray(new LNode[expressions.size()]));} + ; + +for_block returns [LNode node1, LNode node2] + : ^(FOR_BLOCK n1=block n2=block?) + { + $node1 = $n1.node; + $node2 = $n2.node; + } + ; + +attribute returns [LNode node] + : ^(Id expr) {$node = new AttributeNode(new AtomNode($Id.text), $expr.node);} + ; + +table_tag returns [LNode node] +@init{ + List expressions = new ArrayList(); +} + : ^(TABLE + Id {expressions.add(new AtomNode($Id.text));} + lookup {expressions.add($lookup.node);} + block {expressions.add($block.node);} + ^(ATTRIBUTES (attribute {expressions.add($attribute.node);})*) + ) + {$node = new TagNode("tablerow", tags.get("tablerow"), expressions.toArray(new LNode[expressions.size()]));} + ; + +capture_tag returns [LNode node] + : ^(CAPTURE Id block) {$node = new TagNode("capture", tags.get("capture"), new AtomNode($Id.text), $block.node);} + ; + +include_tag returns [LNode node] + : ^(INCLUDE file=Str ^(WITH (with=Str)?)) + { + if($with.text != null) { + $node = new TagNode("include", tags.get("include"), flavor, new AtomNode($file.text), new AtomNode($with.text)); + } else { + $node = new TagNode("include", tags.get("include"), flavor, new AtomNode($file.text)); + } + } + ; + +break_tag returns [LNode node] + : Break {$node = new AtomNode(Tag.Statement.BREAK);} + ; + +continue_tag returns [LNode node] + : Continue {$node = new AtomNode(Tag.Statement.CONTINUE);} + ; + + +custom_tag returns [LNode node] +@init{List expressions = new ArrayList();} + : ^(CUSTOM_TAG Id (expr {expressions.add($expr.node);})*) + {$node = new TagNode($Id.text, tags.get($Id.text), expressions.toArray(new LNode[expressions.size()]));} + ; + +custom_tag_block returns [LNode node] +@init{List expressions = new ArrayList();} + : ^(CUSTOM_TAG_BLOCK Id (expr {expressions.add($expr.node);})* block {expressions.add($block.node);}) + {$node = new TagNode($Id.text, tags.get($Id.text), expressions.toArray(new LNode[expressions.size()]));} + ; + +output returns [OutputNode node] + : ^(OUTPUT expr {$node = new OutputNode($expr.node);} ^(FILTERS (filter {$node.addFilter($filter.node);})*)) + ; + +filter returns [FilterNode node] + : ^(FILTER Id {$node = new FilterNode($Id.text, filters.get($Id.text));} ^(PARAMS params[$node]?)) + ; + +params[FilterNode node] + : (expr {$node.add($expr.node);})+ + ; + +assignment returns [TagNode node] + : ^(ASSIGNMENT Id filter? expr) {$node = new TagNode("assign", tags.get("assign"), new AtomNode($Id.text), $filter.node, $expr.node);} + ; + +expr returns [LNode node] + : ^(Or a=expr b=expr) {$node = new OrNode($a.node, $b.node);} + | ^(And a=expr b=expr) {$node = new AndNode($a.node, $b.node);} + | ^(Eq a=expr b=expr) {$node = new EqNode($a.node, $b.node);} + | ^(NEq a=expr b=expr) {$node = new NEqNode($a.node, $b.node);} + | ^(LtEq a=expr b=expr) {$node = new LtEqNode($a.node, $b.node);} + | ^(Lt a=expr b=expr) {$node = new LtNode($a.node, $b.node);} + | ^(GtEq a=expr b=expr) {$node = new GtEqNode($a.node, $b.node);} + | ^(Gt a=expr b=expr) {$node = new GtNode($a.node, $b.node);} + | ^(Contains a=expr b=expr) {$node = new ContainsNode($a.node, $b.node);} + | LongNum {$node = new AtomNode(new Long($LongNum.text));} + | DoubleNum {$node = new AtomNode(new Double($DoubleNum.text));} + | Str {$node = new AtomNode($Str.text);} + | True {$node = new AtomNode(true);} + | False {$node = new AtomNode(false);} + | Nil {$node = new AtomNode(null);} + | NO_SPACE {$node = new AtomNode($NO_SPACE.text);} + | lookup {$node = $lookup.node;} + | Empty {$node = AtomNode.EMPTY;} + ; + +lookup returns [LookupNode node] + : ^(LOOKUP + FluentPathExpression {$node = new LookupNode($FluentPathExpression.text);} + ) + ; + diff --git a/hapi-fhir-narrativegenerator/src/main/antlr3/ca/uhn/fhir/narrative/template/parser/Liquid.g b/hapi-fhir-narrativegenerator/src/main/antlr3/ca/uhn/fhir/narrative/template/parser/Liquid.g new file mode 100644 index 00000000000..06f8356f0c3 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/antlr3/ca/uhn/fhir/narrative/template/parser/Liquid.g @@ -0,0 +1,545 @@ +/* + * Copyright (c) 2010 by Bart Kiers + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * Project : Liqp; a Liquid Template grammar/parser + * Developed by : Bart Kiers, bart@big-o.nl + */ +grammar Liquid; + +options { + output=AST; + ASTLabelType=CommonTree; +} + +tokens { + ASSIGNMENT; + ATTRIBUTES; + BLOCK; + CAPTURE; + CASE; + COMMENT; + CYCLE; + ELSE; + FILTERS; + FILTER; + FOR_ARRAY; + FOR_BLOCK; + FOR_RANGE; + GROUP; + IF; + ELSIF; + INCLUDE; + LOOKUP; + OUTPUT; + PARAMS; + PLAIN; + RAW; + TABLE; + UNLESS; + WHEN; + WITH; + NO_SPACE; + CUSTOM_TAG; + CUSTOM_TAG_BLOCK; +} + +@parser::header { + package ca.uhn.fhir.narrative.template.parser; +} + +@lexer::header { + package ca.uhn.fhir.narrative.template.parser; +} + +@parser::members { + + private Flavor flavor = Flavor.LIQUID; + + public LiquidParser(Flavor flavor, TokenStream input) { + this(input, new RecognizerSharedState()); + this.flavor = flavor; + } + + @Override + public void reportError(RecognitionException e) { + throw new RuntimeException(e); + } +} + +@lexer::members { + private boolean inTag = false; + private boolean inRaw = false; + + private boolean openRawEndTagAhead() { + + if(!openTagAhead()) { + return false; + } + + int indexLA = 3; + + while(Character.isSpaceChar(input.LA(indexLA))) { + indexLA++; + } + + return input.LA(indexLA) == 'e' && + input.LA(indexLA + 1) == 'n' && + input.LA(indexLA + 2) == 'd' && + input.LA(indexLA + 3) == 'r' && + input.LA(indexLA + 4) == 'a' && + input.LA(indexLA + 5) == 'w'; + } + + private boolean openTagAhead() { + return input.LA(1) == '{' && (input.LA(2) == '{' || input.LA(2) == '\u0025'); + } + + @Override + public void reportError(RecognitionException e) { + throw new RuntimeException(e); + } + + private String strip(String text, boolean singleQuoted) { + return text.substring(1, text.length() - 1); + } +} + +/* parser rules */ +parse + : block EOF -> block + //(t=. {System.out.printf("\%-20s '\%s'\n", tokenNames[$t.type], $t.text);})* EOF + ; + +block + : (options{greedy=true;}: atom)* -> ^(BLOCK atom*) + ; + +atom + : tag + | output + | assignment + | Other -> PLAIN[$Other.text] + ; + +tag + : custom_tag + | raw_tag + | comment_tag + | if_tag + | unless_tag + | case_tag + | cycle_tag + | for_tag + | table_tag + | capture_tag + | include_tag + | break_tag + | continue_tag + ; + +custom_tag + : (TagStart Id (expr (Comma expr)*)? TagEnd -> ^(CUSTOM_TAG Id expr*)) + ((custom_tag_block)=> custom_tag_block -> ^(CUSTOM_TAG_BLOCK Id expr* custom_tag_block))? + ; + +custom_tag_block + : (options{greedy=false;}: atom)* TagStart EndId TagEnd -> ^(BLOCK atom*) + ; + +raw_tag + : TagStart RawStart TagEnd raw_body TagStart RawEnd TagEnd -> raw_body + ; + +raw_body + : other_than_tag_start -> RAW[$other_than_tag_start.text] + ; + +comment_tag + : TagStart CommentStart TagEnd comment_body TagStart CommentEnd TagEnd -> comment_body + ; + +comment_body + : other_than_tag_start -> COMMENT[$other_than_tag_start.text] + ; + +other_than_tag_start + : ~TagStart* + ; + +if_tag + : TagStart IfStart expr TagEnd block elsif_tag* else_tag? TagStart IfEnd TagEnd -> ^(IF expr block elsif_tag* ^(ELSE else_tag?)) + ; + +elsif_tag + : TagStart Elsif expr TagEnd block -> ^(ELSIF expr block) + ; + +else_tag + : TagStart Else TagEnd block -> block + ; + +unless_tag + : TagStart UnlessStart expr TagEnd block else_tag? TagStart UnlessEnd TagEnd -> ^(UNLESS expr block ^(ELSE else_tag?)) + ; + +case_tag + : TagStart CaseStart expr TagEnd Other? when_tag+ else_tag? TagStart CaseEnd TagEnd -> ^(CASE expr when_tag+ ^(ELSE else_tag?)) + ; + +when_tag + : TagStart When term ((Or | Comma) term)* TagEnd block -> ^(WHEN term+ block) + ; + +cycle_tag + : TagStart Cycle cycle_group expr (Comma expr)* TagEnd -> ^(CYCLE cycle_group expr+) + ; + +cycle_group + : ((expr Col)=> expr Col)? -> ^(GROUP expr?) + ; + +for_tag + : for_array + | for_range + ; + +for_array + : TagStart ForStart Id In lookup attribute* TagEnd + for_block + TagStart ForEnd TagEnd + -> ^(FOR_ARRAY Id lookup for_block ^(ATTRIBUTES attribute*)) + ; + +for_range + : TagStart ForStart Id In OPar expr DotDot expr CPar attribute* TagEnd + block + TagStart ForEnd TagEnd + -> ^(FOR_RANGE Id expr expr block ^(ATTRIBUTES attribute*)) + ; + +for_block + : a=block (TagStart Else TagEnd b=block)? -> ^(FOR_BLOCK block block?) + ; + +attribute + : Id Col expr -> ^(Id expr) + ; + +table_tag + : TagStart TableStart Id In lookup attribute* TagEnd block TagStart TableEnd TagEnd -> ^(TABLE Id lookup block ^(ATTRIBUTES attribute*)) + ; + +capture_tag + : TagStart CaptureStart ( Id TagEnd block TagStart CaptureEnd TagEnd -> ^(CAPTURE Id block) + | Str TagEnd block TagStart CaptureEnd TagEnd -> ^(CAPTURE Id[$Str.text] block) + ) + ; + +include_tag + : TagStart Include ( {this.flavor == Flavor.JEKYLL}?=> + file_name_as_str TagEnd -> ^(INCLUDE file_name_as_str ^(WITH )) + | a=Str (With b=Str)? TagEnd -> ^(INCLUDE $a ^(WITH $b?)) + ) + ; + +break_tag + : TagStart Break TagEnd -> Break + ; + +continue_tag + : TagStart Continue TagEnd -> Continue + ; + +output + : OutStart expr filter* OutEnd -> ^(OUTPUT expr ^(FILTERS filter*)) + ; + +filter + : Pipe Id params? -> ^(FILTER Id ^(PARAMS params?)) + ; + +params + : Col expr (Comma expr)* -> expr+ + ; + +assignment + : TagStart Assign Id EqSign expr filter? TagEnd -> ^(ASSIGNMENT Id filter? expr) + ; + +expr + : or_expr + ; + +or_expr + : and_expr (Or^ and_expr)* + ; + +and_expr + : contains_expr (And^ contains_expr)* + ; + +contains_expr + : eq_expr (Contains^ eq_expr)? + ; + +eq_expr + : rel_expr ((Eq | NEq)^ rel_expr)* + ; + +rel_expr + : term ((LtEq | Lt | GtEq | Gt)^ term)? + ; + +term + : DoubleNum + | LongNum + | Str + | True + | False + | Nil + | NoSpace+ -> NO_SPACE[$text] + | lookup + | Empty + | OPar expr CPar -> expr + ; + +lookup + : FluentPathExpression -> ^(LOOKUP FluentPathExpression) + ; + +id + : Id + | Continue -> Id[$Continue.text] + ; + +id2 + : id + | Empty -> Id[$Empty.text] + | CaptureStart -> Id[$CaptureStart.text] + | CaptureEnd -> Id[$CaptureEnd.text] + | CommentStart -> Id[$CommentStart.text] + | CommentEnd -> Id[$CommentEnd.text] + | RawStart -> Id[$RawStart.text] + | RawEnd -> Id[$RawEnd.text] + | IfStart -> Id[$IfStart.text] + | Elsif -> Id[$Elsif.text] + | IfEnd -> Id[$IfEnd.text] + | UnlessStart -> Id[$UnlessStart.text] + | UnlessEnd -> Id[$UnlessEnd.text] + | Else -> Id[$Else.text] + | Contains -> Id[$Contains.text] + | CaseStart -> Id[$CaseStart.text] + | CaseEnd -> Id[$CaseEnd.text] + | When -> Id[$When.text] + | Cycle -> Id[$Cycle.text] + | ForStart -> Id[$ForStart.text] + | ForEnd -> Id[$ForEnd.text] + | In -> Id[$In.text] + | And -> Id[$And.text] + | Or -> Id[$Or.text] + | TableStart -> Id[$TableStart.text] + | TableEnd -> Id[$TableEnd.text] + | Assign -> Id[$Assign.text] + | True -> Id[$True.text] + | False -> Id[$False.text] + | Nil -> Id[$Nil.text] + | Include -> Id[$Include.text] + | With -> Id[$With.text] + | EndId -> Id[$EndId.text] + | Break -> Id[$Break.text] + ; + +file_name_as_str + : other_than_tag_end -> Str[$text] + ; + +other_than_tag_end + : ~TagEnd+ + ; + +/* lexer rules */ +OutStart : '{{' {inTag=true;}; +OutEnd : '}}' {inTag=false;}; +TagStart : '{%' {inTag=true;}; +TagEnd : '%}' {inTag=false;}; + +Str : {inTag}?=> (SStr | DStr); + +DotDot : {inTag}?=> '..'; +Dot : {inTag}?=> '.'; +NEq : {inTag}?=> '!=' | '<>'; +Eq : {inTag}?=> '=='; +EqSign : {inTag}?=> '='; +GtEq : {inTag}?=> '>='; +Gt : {inTag}?=> '>'; +LtEq : {inTag}?=> '<='; +Lt : {inTag}?=> '<'; +Minus : {inTag}?=> '-'; +Pipe : {inTag}?=> '|'; +Col : {inTag}?=> ':'; +Comma : {inTag}?=> ','; +OPar : {inTag}?=> '('; +CPar : {inTag}?=> ')'; +OBr : {inTag}?=> '['; +CBr : {inTag}?=> ']'; +QMark : {inTag}?=> '?'; + +DoubleNum : {inTag}?=> '-'? Digit+ ( {input.LA(1) == '.' && input.LA(2) != '.'}?=> '.' Digit* + | {$type = LongNum;} + ); +LongNum : {inTag}?=> '-'? Digit+; +WS : {inTag}?=> (' ' | '\t' | '\r' | '\n')+ {$channel=HIDDEN;}; + +FluentPathExpression + : {inTag}?=> (Letter | '_') (Letter | '_' | '-' | Digit | '.' | '[' | ']' | '(' | ')' )* + { + if($text.equals("capture")) $type = CaptureStart; + else if($text.equals("endcapture")) $type = CaptureEnd; + else if($text.equals("comment")) $type = CommentStart; + else if($text.equals("endcomment")) $type = CommentEnd; + else if($text.equals("raw")) { $type = RawStart; inRaw = true; } + else if($text.equals("endraw")) { $type = RawEnd; inRaw = false; } + else if($text.equals("if")) $type = IfStart; + else if($text.equals("elsif")) $type = Elsif; + else if($text.equals("endif")) $type = IfEnd; + else if($text.equals("unless")) $type = UnlessStart; + else if($text.equals("endunless")) $type = UnlessEnd; + else if($text.equals("else")) $type = Else; + else if($text.equals("contains")) $type = Contains; + else if($text.equals("case")) $type = CaseStart; + else if($text.equals("endcase")) $type = CaseEnd; + else if($text.equals("when")) $type = When; + else if($text.equals("cycle")) $type = Cycle; + else if($text.equals("for")) $type = ForStart; + else if($text.equals("endfor")) $type = ForEnd; + else if($text.equals("in")) $type = In; + else if($text.equals("and")) $type = And; + else if($text.equals("or")) $type = Or; + else if($text.equals("tablerow")) $type = TableStart; + else if($text.equals("endtablerow")) $type = TableEnd; + else if($text.equals("assign")) $type = Assign; + else if($text.equals("true")) $type = True; + else if($text.equals("false")) $type = False; + else if($text.equals("nil")) $type = Nil; + else if($text.equals("null")) $type = Nil; + else if($text.equals("include")) $type = Include; + else if($text.equals("with")) $type = With; + else if($text.startsWith("end")) $type = EndId; + else if($text.equals("break")) $type = Break; + else if($text.startsWith("continue")) $type = Continue; + else if($text.startsWith("empty")) $type = Empty; + } + ; + + +Id + : {inTag}?=> (Letter | '_') (Letter | '_' | '-' | Digit)* + { + if($text.equals("capture")) $type = CaptureStart; + else if($text.equals("endcapture")) $type = CaptureEnd; + else if($text.equals("comment")) $type = CommentStart; + else if($text.equals("endcomment")) $type = CommentEnd; + else if($text.equals("raw")) { $type = RawStart; inRaw = true; } + else if($text.equals("endraw")) { $type = RawEnd; inRaw = false; } + else if($text.equals("if")) $type = IfStart; + else if($text.equals("elsif")) $type = Elsif; + else if($text.equals("endif")) $type = IfEnd; + else if($text.equals("unless")) $type = UnlessStart; + else if($text.equals("endunless")) $type = UnlessEnd; + else if($text.equals("else")) $type = Else; + else if($text.equals("contains")) $type = Contains; + else if($text.equals("case")) $type = CaseStart; + else if($text.equals("endcase")) $type = CaseEnd; + else if($text.equals("when")) $type = When; + else if($text.equals("cycle")) $type = Cycle; + else if($text.equals("for")) $type = ForStart; + else if($text.equals("endfor")) $type = ForEnd; + else if($text.equals("in")) $type = In; + else if($text.equals("and")) $type = And; + else if($text.equals("or")) $type = Or; + else if($text.equals("tablerow")) $type = TableStart; + else if($text.equals("endtablerow")) $type = TableEnd; + else if($text.equals("assign")) $type = Assign; + else if($text.equals("true")) $type = True; + else if($text.equals("false")) $type = False; + else if($text.equals("nil")) $type = Nil; + else if($text.equals("null")) $type = Nil; + else if($text.equals("include")) $type = Include; + else if($text.equals("with")) $type = With; + else if($text.startsWith("end")) $type = EndId; + else if($text.equals("break")) $type = Break; + else if($text.startsWith("continue")) $type = Continue; + else if($text.startsWith("empty")) $type = Empty; + } + ; + +Other + : ({!inTag && !openTagAhead()}?=> . )+ + | ({!inTag && inRaw && !openRawEndTagAhead()}?=> . )+ + ; + +NoSpace + : ~(' ' | '\t' | '\r' | '\n') + ; + +/* fragment rules */ +fragment Letter : 'a'..'z' | 'A'..'Z'; +fragment Digit : '0'..'9'; +fragment SStr : '\'' ~'\''* '\'' {setText(strip($text, true));}; +fragment DStr : '"' ~'"'* '"' {setText(strip($text, false));}; + +fragment CommentStart : 'CommentStart'; +fragment CommentEnd : 'CommentEnd'; +fragment RawStart : 'RawStart'; +fragment RawEnd : 'RawEnd'; +fragment IfStart : 'IfStart'; +fragment IfEnd : 'IfEnd'; +fragment Elsif : 'Elsif'; +fragment UnlessStart : 'UnlessStart'; +fragment UnlessEnd : 'UnlessEnd'; +fragment Else : 'Else'; +fragment Contains : 'contains'; +fragment CaseStart : 'CaseStart'; +fragment CaseEnd : 'CaseEnd'; +fragment When : 'When'; +fragment Cycle : 'Cycle'; +fragment ForStart : 'ForStart'; +fragment ForEnd : 'ForEnd'; +fragment In : 'In'; +fragment And : 'And'; +fragment Or : 'Or'; +fragment TableStart : 'TableStart'; +fragment TableEnd : 'TableEnd'; +fragment Assign : 'Assign'; +fragment True : 'True'; +fragment False : 'False'; +fragment Nil : 'Nil'; +fragment Include : 'Include'; +fragment With : 'With'; +fragment CaptureStart : 'CaptureStart'; +fragment CaptureEnd : 'CaptureEnd'; +fragment EndId : 'EndId'; +fragment Break : 'Break'; +fragment Continue : 'Continue'; +fragment Empty : 'Empty'; diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/LValue.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/LValue.java new file mode 100644 index 00000000000..88f07d96ca4 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/LValue.java @@ -0,0 +1,248 @@ +package ca.uhn.fhir.narrative.template; + +import java.util.Collection; +import java.util.List; + +import ca.uhn.fhir.narrative.template.nodes.AtomNode; + +/** + * An abstract class the Filter and Tag classes extend. + *

+ * It houses some utility methods easily available for said + * classes. + */ +public abstract class LValue { + + /** + * Returns true iff a and b are equals, where (int) 1 is + * equals to (double) 1.0 + * + * @param a + * the first object to compare. + * @param b + * the second object to compare. + * + * @return true iff a and b are equals, where (int) 1 is + * equals to (double) 1.0 + */ + public static boolean areEqual(Object a, Object b) { + + if (a == b) { + return true; + } + + if (a == null || b == null) { + return false; + } + + // TODO refactor the instance-ofs below + + if (a instanceof Number && b instanceof Number) { + + double delta = ((Number) a).doubleValue() - ((Number) b).doubleValue(); + + // To account for floating point rounding errors, return true if + // the difference between double a and double b is very small. + return Math.abs(delta) < 0.00000000001; + } + + if (AtomNode.isEmpty(a) && (b instanceof CharSequence)) { + return ((CharSequence)b).length() == 0; + } + + if (AtomNode.isEmpty(b) && (a instanceof CharSequence)) { + return ((CharSequence)a).length() == 0; + } + + if (AtomNode.isEmpty(a) && (b instanceof Collection)) { + return ((Collection)b).size() == 0; + } + + if (AtomNode.isEmpty(b) && (a instanceof Collection)) { + return ((Collection)a).size() == 0; + } + + if (AtomNode.isEmpty(a) && (b.getClass().isArray())) { + return ((Object[])b).length == 0; + } + + if (AtomNode.isEmpty(b) && (a.getClass().isArray())) { + return ((Object[])a).length == 0; + } + + return a.equals(b); + } + + /** + * Returns this value as an array. If a value is already an array, + * it is casted to a `Object[]`, if it's a `java.util.List`, it is + * converted to an array and in all other cases, `value` is simply + * returned as an `Object[]` with a single value in it. + * + * @param value + * the value to convert/cast to an array. + * + * @return this value as an array. + */ + public Object[] asArray(Object value) { + + if(value == null) { + return null; + } + + if (value.getClass().isArray()) { + return (Object[]) value; + } + + if (value instanceof List) { + return ((List) value).toArray(); + } + + return new Object[]{value}; + } + + /** + * Convert `value` to a boolean. Note that only `nil` and `false` + * are `false`, all other values are `true`. + * + * @param value + * the value to convert. + * + * @return `value` as a boolean. + */ + public boolean asBoolean(Object value) { + + if (value == null) { + return false; + } + + if (value instanceof Boolean) { + return (Boolean) value; + } + + return true; + } + + /** + * Returns `value` as a Number. Strings will be coerced into + * either a Long or Double. + * + * @param value + * the value to cast to a Number. + * + * @return `value` as a Number. + * + * @throws NumberFormatException when `value` is a String which could + * not be parsed as a Long or Double. + */ + public Number asNumber(Object value) throws NumberFormatException { + + if(value instanceof Number) { + return (Number) value; + } + + String str = String.valueOf(value); + + return str.matches("\\d+") ? Long.valueOf(str) : Double.valueOf(str); + } + + /** + * Returns `value` as a String. + * + * @param value + * the value to convert to a String. + * + * @return `value` as a String. + */ + public String asString(Object value) { + + if (value == null) { + return ""; + } + + if (!this.isArray(value)) { + return String.valueOf(value); + } + + Object[] array = this.asArray(value); + + StringBuilder builder = new StringBuilder(); + + for (Object obj : array) { + builder.append(this.asString(obj)); + } + + return builder.toString(); + } + + /** + * Returns true iff `value` is an array or a java.util.List. + * + * @param value + * the value to check. + * + * @return true iff `value` is an array or a java.util.List. + */ + public boolean isArray(Object value) { + + return value != null && (value.getClass().isArray() || value instanceof List); + } + + /** + * Returns true iff `value` is a whole number (Integer or Long). + * + * @param value + * the value to check. + * + * @return true iff `value` is a whole number (Integer or Long). + */ + public boolean isInteger(Object value) { + return value != null && (value instanceof Long || value instanceof Integer); + } + + /** + * Returns true iff `value` is a Number. + * + * @param value + * the value to check. + * + * @return true iff `value` is a Number. + */ + public boolean isNumber(Object value) { + + if(value == null) { + return false; + } + + if(value instanceof Number) { + return true; + } + + // valid Long? + if(String.valueOf(value).matches("\\d+")) { + return true; + } + + try { + // valid Double? + Double.parseDouble(String.valueOf(value)); + } catch(Exception e) { + return false; + } + + return true; + } + + /** + * Returns true iff `value` is a String. + * + * @param value + * the value to check. + * + * @return true iff `value` is a String. + */ + public boolean isString(Object value) { + + return value != null && value instanceof CharSequence; + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/Template.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/Template.java new file mode 100644 index 00000000000..2f365ef8399 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/Template.java @@ -0,0 +1,248 @@ +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 + * parse(...) to get a hold of a reference. + *

+ * 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 tags; + + /** + * This instance's filters. + */ + private final Map 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 tags, Map filters) { + this(input, tags, filters, Flavor.LIQUID); + } + + private Template(String input, Map tags, Map 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 tags, Map filters) throws IOException { + this(file, tags, filters, Flavor.LIQUID); + } + + private Template(File file, Map tags, Map 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 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. + *

+ * 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 firstStack = new ArrayList(); + firstStack.add(tree); + + List> childListStack = new ArrayList>(); + childListStack.add(firstStack); + + while (!childListStack.isEmpty()) { + + List 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((List) tree.getChildren())); + } + } + } + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/TemplateNarrativeGenerator.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/TemplateNarrativeGenerator.java new file mode 100644 index 00000000000..a1d6a35da3e --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/TemplateNarrativeGenerator.java @@ -0,0 +1,101 @@ + +package ca.uhn.fhir.narrative.template; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.antlr.runtime.tree.CommonTree; +import org.hl7.fhir.dstu3.exceptions.FHIRException; +import org.hl7.fhir.dstu3.hapi.validation.DefaultProfileValidationSupport; +import org.hl7.fhir.dstu3.hapi.validation.HapiWorkerContext; +import org.hl7.fhir.dstu3.hapi.validation.IValidationSupport; +import org.hl7.fhir.dstu3.model.Base; +import org.hl7.fhir.dstu3.utils.FluentPathEngine; +import org.hl7.fhir.dstu3.utils.IWorkerContext; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.INarrative; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.narrative.INarrativeGenerator; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.util.FhirTerser; + +public class TemplateNarrativeGenerator implements INarrativeGenerator { + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TemplateNarrativeGenerator.class); + + private FhirContext myFhirContext = FhirContext.forDstu3(); + private IValidationSupport myValidationSupport = new DefaultProfileValidationSupport(); + + @Override + public void generateNarrative(FhirContext theContext, IBaseResource theResource, INarrative theNarrative) { + // TODO Auto-generated method stub + + } + + public String processLiquid(FhirContext theContext, String theTemplate, IBaseResource theResource) { + Template template = Template.parse(theTemplate); + + ourLog.info(template.toStringAST()); + + IWorkerContext ctx = new HapiWorkerContext(myFhirContext, myValidationSupport); + FluentPathEngine fluentPathEngine = new FluentPathEngine(ctx); + + FhirTerser terser = new FhirTerser(theContext); + + HashMap context = new HashMap(); + context.put("context", theResource); + context.put("terser", terser); + context.put("fpEngine", fluentPathEngine); + + return template.render(context); + + } + + class MyContextMap extends HashMap { + private static final long serialVersionUID = 1L; + private List myContext; + private IBaseResource myResource; + + MyContextMap(IBaseResource theResource) { + myResource = theResource; + myContext = null; + } + + MyContextMap(Base theContext, IBaseResource theResource) { + myContext = Collections.singletonList(theContext); + myResource=theResource; + } + + MyContextMap(List theContext, IBaseResource theResource) { + myContext = (theContext); + myResource=theResource; + } + + @Override + public Object get(Object theKey) { + ourLog.info("Requesting key: {}", theKey); + + if (theKey.equals("resource")) { + return new MyContextMap((Base) myResource, myResource); + } + + IWorkerContext ctx = new HapiWorkerContext(myFhirContext, myValidationSupport); + try { + FluentPathEngine fluentPathEngine = new FluentPathEngine(ctx); + List evaluated = new ArrayList(); + for (Base nextContext : myContext) { + evaluated.addAll(fluentPathEngine.evaluate(nextContext, (String)theKey)); + } + return new MyContextMap(evaluated, myResource); + } catch (FHIRException e) { + throw new InternalErrorException("Failed to process expression: " + theKey, e); + } + + } + } + +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Append.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Append.java new file mode 100644 index 00000000000..2c4d8ada2db --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Append.java @@ -0,0 +1,15 @@ +package ca.uhn.fhir.narrative.template.filters; + +class Append extends Filter { + + /* + * (Object) append(input, string) + * + * add one string to another + */ + @Override + public Object apply(Object value, Object... params) { + + return super.asString(value) + super.asString(super.get(0, params)); + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Capitalize.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Capitalize.java new file mode 100644 index 00000000000..fab45b82a3a --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Capitalize.java @@ -0,0 +1,23 @@ +package ca.uhn.fhir.narrative.template.filters; + +class Capitalize extends Filter { + + /* + * (Object) capitalize(input) + * + * capitalize words in the input sentence + */ + @Override + public Object apply(Object value, Object... params) { + + String original = super.asString(value); + + if (original.isEmpty()) { + return original; + } + + char first = original.charAt(0); + + return Character.toUpperCase(first) + original.substring(1); + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Date.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Date.java new file mode 100644 index 00000000000..c4510d7b392 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Date.java @@ -0,0 +1,255 @@ +package ca.uhn.fhir.narrative.template.filters; + +import java.text.SimpleDateFormat; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +class Date extends Filter { + + private static Locale locale = Locale.ENGLISH; + private static Set datePatterns = new HashSet(); + + private final static java.util.Map LIQUID_TO_JAVA_FORMAT = + new java.util.HashMap(); + + static { + addDatePattern("yyyy-MM-dd HH:mm:ss"); + addDatePattern("EEE MMM ddhh:mm:ss yyyy"); + init(); + } + + /* + * (Object) date(input, format) + * + * Reformat a date + * + * %a - The abbreviated weekday name (``Sun'') + * %A - The full weekday name (``Sunday'') + * %b - The abbreviated month name (``Jan'') + * %B - The full month name (``January'') + * %c - The preferred local date and time representation + * %d - Day of the month (01..31) + * %H - Hour of the day, 24-hour clock (00..23) + * %I - Hour of the day, 12-hour clock (01..12) + * %j - Day of the year (001..366) + * %m - Month of the year (01..12) + * %M - Minute of the hour (00..59) + * %p - Meridian indicator (``AM'' or ``PM'') + * %S - Second of the minute (00..60) + * %U - Week number of the current year, + * starting with the first Sunday as the first + * day of the first week (00..53) + * %W - Week number of the current year, + * starting with the first Monday as the first + * day of the first week (00..53) + * %w - Day of the week (Sunday is 0, 0..6) + * %x - Preferred representation for the date alone, no time + * %X - Preferred representation for the time alone, no date + * %y - Year without a century (00..99) + * %Y - Year with century + * %Z - Time zone name + * %% - Literal ``%'' character + */ + @Override + public Object apply(Object value, Object... params) { + + try { + final Long seconds; + + if(super.asString(value).equals("now")) { + seconds = System.currentTimeMillis() / 1000L; + } + else if(super.isNumber(value)) { + // No need to divide this by 1000, the param is expected to be in seconds already! + seconds = super.asNumber(value).longValue(); + } + else { + seconds = trySeconds(super.asString(value)); // formatter.parse(super.asString(value)).getTime() / 1000L; + + if(seconds == null) { + return value; + } + } + + final java.util.Date date = new java.util.Date(seconds * 1000L); + final String format = super.asString(super.get(0, params)); + + if(format == null || format.trim().isEmpty()) { + return value; + } + + final java.util.Calendar calendar = java.util.Calendar.getInstance(); + calendar.setTime(date); + + StringBuilder builder = new StringBuilder(); + + for (int i = 0; i < format.length(); i++) { + + char ch = format.charAt(i); + + if (ch == '%') { + + i++; + + if (i == format.length()) { + // a trailing (single) '%' sign: just append it + builder.append("%"); + break; + } + + char next = format.charAt(i); + + SimpleDateFormat javaFormat = LIQUID_TO_JAVA_FORMAT.get(next); + + if (javaFormat == null) { + // no valid date-format: append the '%' and the 'next'-char + builder.append("%").append(next); + } + else { + builder.append(javaFormat.format(date)); + } + } + else { + builder.append(ch); + } + } + + return builder.toString(); + } + catch (Exception e) { + return value; + } + } + + private static void init() { + + // %% - Literal ``%'' character + LIQUID_TO_JAVA_FORMAT.put('%', new SimpleDateFormat("%", locale)); + + // %a - The abbreviated weekday name (``Sun'') + LIQUID_TO_JAVA_FORMAT.put('a', new SimpleDateFormat("EEE", locale)); + + // %A - The full weekday name (``Sunday'') + LIQUID_TO_JAVA_FORMAT.put('A', new SimpleDateFormat("EEEE", locale)); + + // %b - The abbreviated month name (``Jan'') + LIQUID_TO_JAVA_FORMAT.put('b', new SimpleDateFormat("MMM", locale)); + LIQUID_TO_JAVA_FORMAT.put('h', new SimpleDateFormat("MMM", locale)); + + // %B - The full month name (``January'') + LIQUID_TO_JAVA_FORMAT.put('B', new SimpleDateFormat("MMMM", locale)); + + // %c - The preferred local date and time representation + LIQUID_TO_JAVA_FORMAT.put('c', new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy", locale)); + + // %d - Day of the month (01..31) + LIQUID_TO_JAVA_FORMAT.put('d', new SimpleDateFormat("dd", locale)); + + // %H - Hour of the day, 24-hour clock (00..23) + LIQUID_TO_JAVA_FORMAT.put('H', new SimpleDateFormat("HH", locale)); + + // %I - Hour of the day, 12-hour clock (01..12) + LIQUID_TO_JAVA_FORMAT.put('I', new SimpleDateFormat("hh", locale)); + + // %j - Day of the year (001..366) + LIQUID_TO_JAVA_FORMAT.put('j', new SimpleDateFormat("DDD", locale)); + + // %m - Month of the year (01..12) + LIQUID_TO_JAVA_FORMAT.put('m', new SimpleDateFormat("MM", locale)); + + // %M - Minute of the hour (00..59) + LIQUID_TO_JAVA_FORMAT.put('M', new SimpleDateFormat("mm", locale)); + + // %p - Meridian indicator (``AM'' or ``PM'') + LIQUID_TO_JAVA_FORMAT.put('p', new SimpleDateFormat("a", locale)); + + // %S - Second of the minute (00..60) + LIQUID_TO_JAVA_FORMAT.put('S', new SimpleDateFormat("ss", locale)); + + // %U - Week number of the current year, + // starting with the first Sunday as the first + // day of the first week (00..53) + LIQUID_TO_JAVA_FORMAT.put('U', new SimpleDateFormat("ww", locale)); + + // %W - Week number of the current year, + // starting with the first Monday as the first + // day of the first week (00..53) + LIQUID_TO_JAVA_FORMAT.put('W', new SimpleDateFormat("ww", locale)); + + // %w - Day of the week (Sunday is 0, 0..6) + LIQUID_TO_JAVA_FORMAT.put('w', new SimpleDateFormat("F", locale)); + + // %x - Preferred representation for the date alone, no time + LIQUID_TO_JAVA_FORMAT.put('x', new SimpleDateFormat("MM/dd/yy", locale)); + + // %X - Preferred representation for the time alone, no date + LIQUID_TO_JAVA_FORMAT.put('X', new SimpleDateFormat("HH:mm:ss", locale)); + + // %y - Year without a century (00..99) + LIQUID_TO_JAVA_FORMAT.put('y', new SimpleDateFormat("yy", locale)); + + // %Y - Year with century + LIQUID_TO_JAVA_FORMAT.put('Y', new SimpleDateFormat("yyyy", locale)); + + // %Z - Time zone name + LIQUID_TO_JAVA_FORMAT.put('Z', new SimpleDateFormat("z", locale)); + } + + /** + * Changes the locale. + * + * @param locale the new locale. + */ + public static void setLocale(Locale locale) { + Date.locale = locale; + init(); + } + + /** + * Adds a new Date-pattern to be used when parsing a string to a Date. + * + * @param pattern the pattern. + */ + public static void addDatePattern(String pattern) { + + if(pattern == null) { + throw new NullPointerException("date-pattern cannot be null"); + } + + datePatterns.add(pattern); + } + + /** + * Removed a Date-pattern to be used when parsing a string to a Date. + * + * @param pattern the pattern. + */ + public static void removeDatePattern(String pattern) { + + datePatterns.remove(pattern); + } + + /* + * Try to parse `str` into a Date and return this Date as seconds + * since EPOCH, or null if it could not be parsed. + */ + private Long trySeconds(String str) { + + for(String pattern : datePatterns) { + + SimpleDateFormat parser = new SimpleDateFormat(pattern, locale); + + try { + long milliseconds = parser.parse(str).getTime(); + return milliseconds / 1000L; + } + catch(Exception e) { + // Just ignore and try the next pattern in `datePatterns`. + } + } + + // Could not parse the string into a meaningful date, return null. + return null; + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Divided_By.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Divided_By.java new file mode 100644 index 00000000000..0ebc783a8c1 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Divided_By.java @@ -0,0 +1,31 @@ +package ca.uhn.fhir.narrative.template.filters; + +class Divided_By extends Filter { + + /* + * divided_by(input, operand) + * + * division + */ + @Override + public Object apply(Object value, Object... params) { + + if(value == null) { + value = 0L; + } + + super.checkParams(params, 1); + + Object rhsObj = params[0]; + + if(super.asNumber(rhsObj).doubleValue() == 0.0) { + throw new RuntimeException("Liquid error: divided by 0"); + } + + if (super.isInteger(value) && super.isInteger(rhsObj)) { + return super.asNumber(value).longValue() / super.asNumber(rhsObj).longValue(); + } + + return super.asNumber(value).doubleValue() / super.asNumber(rhsObj).doubleValue(); + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Downcase.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Downcase.java new file mode 100644 index 00000000000..05db032cfbd --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Downcase.java @@ -0,0 +1,15 @@ +package ca.uhn.fhir.narrative.template.filters; + +class Downcase extends Filter { + + /* + * downcase(input) + * + * convert a input string to DOWNCASE + */ + @Override + public Object apply(Object value, Object... params) { + + return super.asString(value).toLowerCase(); + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Escape.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Escape.java new file mode 100644 index 00000000000..632a423a02c --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Escape.java @@ -0,0 +1,20 @@ +package ca.uhn.fhir.narrative.template.filters; + +class Escape extends Filter { + + /* + * escape(input) + * + * escape a string + */ + @Override + public Object apply(Object value, Object... params) { + + String str = super.asString(value); + + return str.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """); + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Escape_Once.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Escape_Once.java new file mode 100644 index 00000000000..ee024cb96c9 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Escape_Once.java @@ -0,0 +1,21 @@ +package ca.uhn.fhir.narrative.template.filters; + +class Escape_Once extends Filter { + + /* + * escape_once(input) + * + * returns an escaped version of html without affecting + * existing escaped entities + */ + @Override + public Object apply(Object value, Object... params) { + + String str = super.asString(value); + + return str.replaceAll("&(?!([a-zA-Z]+|#[0-9]+|#x[0-9A-Fa-f]+);)", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """); + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Filter.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Filter.java new file mode 100644 index 00000000000..b96e49ed493 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Filter.java @@ -0,0 +1,169 @@ +package ca.uhn.fhir.narrative.template.filters; + +import ca.uhn.fhir.narrative.template.LValue; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * Output markup takes filters. Filters are simple methods. The first + * parameter is always the output of the left side of the filter. The + * return value of the filter will be the new left value when the next + * filter is run. When there are no more filters, the template will + * receive the resulting string. + *

+ * -- https://github.com/Shopify/liquid/wiki/Liquid-for-Designers + */ +public abstract class Filter extends LValue { + + /** + * A map holding all filters. + */ + private static final Map FILTERS = new HashMap(); + + static { + // Initialize all standard filters. + registerFilter(new Append()); + registerFilter(new Capitalize()); + registerFilter(new Date()); + registerFilter(new Divided_By()); + registerFilter(new Downcase()); + registerFilter(new Escape()); + registerFilter(new Escape_Once()); + registerFilter(new First()); + registerFilter(new H()); + registerFilter(new Join()); + registerFilter(new Last()); + registerFilter(new ca.uhn.fhir.narrative.template.filters.Map()); + registerFilter(new Minus()); + registerFilter(new Modulo()); + registerFilter(new Newline_To_Br()); + registerFilter(new Plus()); + registerFilter(new Prepend()); + registerFilter(new Remove()); + registerFilter(new Remove_First()); + registerFilter(new Replace()); + registerFilter(new Replace_First()); + registerFilter(new Size()); + registerFilter(new Sort()); + registerFilter(new Split()); + registerFilter(new Strip_HTML()); + registerFilter(new Strip_Newlines()); + registerFilter(new Times()); + registerFilter(new Truncate()); + registerFilter(new Truncatewords()); + registerFilter(new Upcase()); + } + + /** + * The name of the filter. + */ + public final String name; + + /** + * Used for all package protected filters in the liqp.filters-package + * whose name is their class name lower cased. + */ + protected Filter() { + this.name = this.getClass().getSimpleName().toLowerCase(); + } + + /** + * Creates a new instance of a Filter. + * + * @param name + * the name of the filter. + */ + public Filter(String name) { + this.name = name; + } + + /** + * Applies the filter on the 'value'. + * + * @param value + * the string value `AAA` in: `{{ 'AAA' | f:1,2,3 }}` + * @param params + * the values [1, 2, 3] in: `{{ 'AAA' | f:1,2,3 }}` + * + * @return the result of the filter. + */ + public abstract Object apply(Object value, Object... params); + + /** + * Check the number of parameters and throws an exception if needed. + * + * @param params + * the parameters to check. + * @param expected + * the expected number of parameters. + */ + public final void checkParams(Object[] params, int expected) { + if(params == null || params.length != expected) { + throw new RuntimeException("Liquid error: wrong number of arguments (" + + (params == null ? 0 : params.length + 1) + " for " + (expected + 1) + ")"); + } + } + + /** + * Returns a value at a specific index from an array of parameters. + * If no such index exists, a RuntimeException is thrown. + * + * @param index + * the index of the value to be retrieved. + * @param params + * the values. + * + * @return a value at a specific index from an array of + * parameters. + */ + protected Object get(int index, Object... params) { + + if (index >= params.length) { + throw new RuntimeException("error in filter '" + name + + "': cannot get param index: " + index + + " from: " + Arrays.toString(params)); + } + + return params[index]; + } + + /** + * Retrieves a filter with a specific name. + * + * @param name + * the name of the filter to retrieve. + * + * @return a filter with a specific name. + */ + public static Filter getFilter(String name) { + + Filter filter = FILTERS.get(name); + + if (filter == null) { + throw new RuntimeException("unknown filter: " + name); + } + + return filter; + } + + /** + * Returns all default filters. + * + * @return all default filters. + */ + public static Map getFilters() { + return new HashMap(FILTERS); + } + + /** + * Registers a new filter. + * + * @param filter + * the filter to be registered. + */ + public static void registerFilter(Filter filter) { + FILTERS.put(filter.name, filter); + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/First.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/First.java new file mode 100644 index 00000000000..d11a5aafe74 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/First.java @@ -0,0 +1,17 @@ +package ca.uhn.fhir.narrative.template.filters; + +class First extends Filter { + + /* + * first(array) + * + * Get the first element of the passed in array + */ + @Override + public Object apply(Object value, Object... params) { + + Object[] array = super.asArray(value); + + return array.length == 0 ? null : super.asString(array[0]); + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/H.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/H.java new file mode 100644 index 00000000000..46a1f59937e --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/H.java @@ -0,0 +1,15 @@ +package ca.uhn.fhir.narrative.template.filters; + +class H extends Filter { + + /* + * h(input) + * + * Alias for: escape + */ + @Override + public Object apply(Object value, Object... params) { + + return Filter.getFilter("escape").apply(value, params); + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Join.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Join.java new file mode 100644 index 00000000000..4334c46051a --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Join.java @@ -0,0 +1,35 @@ +package ca.uhn.fhir.narrative.template.filters; + +import java.util.Arrays; + +class Join extends Filter { + + /* + * join(input, glue = ' ') + * + * Join elements of the array with certain character between them + */ + @Override + public Object apply(Object value, Object... params) { + + if (value == null) { + return ""; + } + + StringBuilder builder = new StringBuilder(); + + Object[] array = super.asArray(value); + String glue = params.length == 0 ? " " : super.asString(super.get(0, params)); + + for (int i = 0; i < array.length; i++) { + + builder.append(super.asString(array[i])); + + if (i < array.length - 1) { + builder.append(glue); + } + } + + return builder.toString(); + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Last.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Last.java new file mode 100644 index 00000000000..66a151aa132 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Last.java @@ -0,0 +1,17 @@ +package ca.uhn.fhir.narrative.template.filters; + +class Last extends Filter { + + /* + * last(array) + * + * Get the last element of the passed in array + */ + @Override + public Object apply(Object value, Object... params) { + + Object[] array = super.asArray(value); + + return array.length == 0 ? null : super.asString(array[array.length - 1]); + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Map.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Map.java new file mode 100644 index 00000000000..d2a9615f53f --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Map.java @@ -0,0 +1,39 @@ +package ca.uhn.fhir.narrative.template.filters; + +import java.util.ArrayList; +import java.util.List; + +class Map extends Filter { + + /* + * map(input, property) + * + * map/collect on a given property + */ + @Override + public Object apply(Object value, Object... params) { + + if (value == null) { + return ""; + } + + List list = new ArrayList(); + + Object[] array = super.asArray(value); + + String key = super.asString(super.get(0, params)); + + for (Object obj : array) { + + java.util.Map map = (java.util.Map) obj; + + Object val = map.get(key); + + if (val != null) { + list.add(val); + } + } + + return list.toArray(new Object[list.size()]); + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Minus.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Minus.java new file mode 100644 index 00000000000..d79616a42a5 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Minus.java @@ -0,0 +1,27 @@ +package ca.uhn.fhir.narrative.template.filters; + +class Minus extends Filter { + + /* + * plus(input, operand) + * + * subtraction + */ + @Override + public Object apply(Object value, Object... params) { + + if(value == null) { + value = 0L; + } + + super.checkParams(params, 1); + + Object rhsObj = params[0]; + + if (super.isInteger(value) && super.isInteger(rhsObj)) { + return super.asNumber(value).longValue() - super.asNumber(rhsObj).longValue(); + } + + return super.asNumber(value).doubleValue() - super.asNumber(rhsObj).doubleValue(); + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Modulo.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Modulo.java new file mode 100644 index 00000000000..aaeddc665e8 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Modulo.java @@ -0,0 +1,27 @@ +package ca.uhn.fhir.narrative.template.filters; + +class Modulo extends Filter { + + /* + * plus(input, operand) + * + * modulus + */ + @Override + public Object apply(Object value, Object... params) { + + if(value == null) { + value = 0L; + } + + super.checkParams(params, 1); + + Object rhsObj = params[0]; + + if (super.isInteger(value) && super.isInteger(rhsObj)) { + return super.asNumber(value).longValue() % super.asNumber(rhsObj).longValue(); + } + + return super.asNumber(value).doubleValue() % super.asNumber(rhsObj).doubleValue(); + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Newline_To_Br.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Newline_To_Br.java new file mode 100644 index 00000000000..b300a23acd0 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Newline_To_Br.java @@ -0,0 +1,15 @@ +package ca.uhn.fhir.narrative.template.filters; + +class Newline_To_Br extends Filter { + + /* + * newline_to_br(input) + * + * Add
tags in front of all newlines in input string + */ + @Override + public Object apply(Object value, Object... params) { + + return super.asString(value).replaceAll("[\n]", "
\n"); + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Plus.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Plus.java new file mode 100644 index 00000000000..5e54805778b --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Plus.java @@ -0,0 +1,27 @@ +package ca.uhn.fhir.narrative.template.filters; + +class Plus extends Filter { + + /* + * plus(input, operand) + * + * addition + */ + @Override + public Object apply(Object value, Object... params) { + + if(value == null) { + value = 0L; + } + + super.checkParams(params, 1); + + Object rhsObj = params[0]; + + if (super.isInteger(value) && super.isInteger(rhsObj)) { + return super.asNumber(value).longValue() + super.asNumber(rhsObj).longValue(); + } + + return super.asNumber(value).doubleValue() + super.asNumber(rhsObj).doubleValue(); + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Prepend.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Prepend.java new file mode 100644 index 00000000000..b1aedbb6751 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Prepend.java @@ -0,0 +1,15 @@ +package ca.uhn.fhir.narrative.template.filters; + +class Prepend extends Filter { + + /* + * (Object) append(input, string) + * + * add one string to another + */ + @Override + public Object apply(Object value, Object... params) { + + return super.asString(super.get(0, params)) + super.asString(value); + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Remove.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Remove.java new file mode 100644 index 00000000000..9d616f9a2e4 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Remove.java @@ -0,0 +1,23 @@ +package ca.uhn.fhir.narrative.template.filters; + +class Remove extends Filter { + + /* + * remove(input, string) + * + * remove a substring + */ + @Override + public Object apply(Object value, Object... params) { + + String original = super.asString(value); + + Object needle = super.get(0, params); + + if (needle == null) { + throw new RuntimeException("invalid pattern: " + needle); + } + + return original.replace(String.valueOf(needle), ""); + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Remove_First.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Remove_First.java new file mode 100644 index 00000000000..28ad879ff83 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Remove_First.java @@ -0,0 +1,25 @@ +package ca.uhn.fhir.narrative.template.filters; + +import java.util.regex.Pattern; + +class Remove_First extends Filter { + + /* + * remove_first(input, string) + * + * remove the first occurrences of a substring + */ + @Override + public Object apply(Object value, Object... params) { + + String original = super.asString(value); + + Object needle = super.get(0, params); + + if (needle == null) { + throw new RuntimeException("invalid pattern: " + needle); + } + + return original.replaceFirst(Pattern.quote(String.valueOf(needle)), ""); + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Replace.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Replace.java new file mode 100644 index 00000000000..3668898b399 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Replace.java @@ -0,0 +1,35 @@ +package ca.uhn.fhir.narrative.template.filters; + +class Replace extends Filter { + + /* + * replace(input, string, replacement = '') + * + * Replace occurrences of a string with another + */ + @Override + public Object apply(Object value, Object... params) { + + String original = super.asString(value); + + Object needle = super.get(0, params); + String replacement = ""; + + if (needle == null) { + throw new RuntimeException("invalid pattern: " + needle); + } + + if (params.length >= 2) { + + Object obj = super.get(1, params); + + if (obj == null) { + throw new RuntimeException("invalid replacement: " + needle); + } + + replacement = super.asString(super.get(1, params)); + } + + return original.replace(String.valueOf(needle), String.valueOf(replacement)); + } +} \ No newline at end of file diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Replace_First.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Replace_First.java new file mode 100644 index 00000000000..cb09418c532 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Replace_First.java @@ -0,0 +1,39 @@ +package ca.uhn.fhir.narrative.template.filters; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +class Replace_First extends Filter { + + /* + * replace_first(input, string, replacement = '') + * + * Replace the first occurrences of a string with another + */ + @Override + public Object apply(Object value, Object... params) { + + String original = super.asString(value); + + Object needle = super.get(0, params); + String replacement = ""; + + if (needle == null) { + throw new RuntimeException("invalid pattern: " + needle); + } + + if (params.length >= 2) { + + Object obj = super.get(1, params); + + if (obj == null) { + throw new RuntimeException("invalid replacement: " + needle); + } + + replacement = super.asString(super.get(1, params)); + } + + return original.replaceFirst(Pattern.quote(String.valueOf(needle)), + Matcher.quoteReplacement(replacement)); + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Size.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Size.java new file mode 100644 index 00000000000..dcff5478bc3 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Size.java @@ -0,0 +1,30 @@ +package ca.uhn.fhir.narrative.template.filters; + +class Size extends Filter { + + /* + * size(input) + * + * Return the size of an array or of an string + */ + @Override + public Object apply(Object value, Object... params) { + + if (super.isArray(value)) { + return super.asArray(value).length; + } + + if (super.isString(value)) { + return super.asString(value).length(); + } + + if (super.isNumber(value)) { + // we're only using 64 bit longs, no BigIntegers or the like. + // So just return 8 (the number of bytes in a long). + return 8; + } + + // boolean or nil + return 0; + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Sort.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Sort.java new file mode 100644 index 00000000000..a8d247e5f40 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Sort.java @@ -0,0 +1,87 @@ +package ca.uhn.fhir.narrative.template.filters; + +import java.util.*; +import java.util.Map; + +class Sort extends Filter { + + /* + * sort(input, property = nil) + * + * Sort elements of the array provide optional property with + * which to sort an array of hashes or drops + */ + @Override + public Object apply(Object value, Object... params) { + + if (value == null) { + return ""; + } + + if(!super.isArray(value)) { + throw new RuntimeException("cannot sort: " + value); + } + + Object[] array = super.asArray(value); + String property = params.length == 0 ? null : super.asString(params[0]); + + List list = asComparableList(array, property); + + Collections.sort(list); + + return property == null ? + list.toArray(new Comparable[list.size()]) : + list.toArray(new SortableMap[list.size()]); + } + + private List asComparableList(Object[] array, String property) { + + List list = new ArrayList(); + + for (Object obj : array) { + + if(obj instanceof java.util.Map && property != null) { + list.add(new SortableMap((java.util.Map)obj, property)); + } + else { + list.add((Comparable) obj); + } + } + + return list; + } + + static class SortableMap extends HashMap implements Comparable { + + final String property; + + SortableMap(java.util.Map map, String property) { + super.putAll(map); + this.property = property; + } + + @Override + public int compareTo(SortableMap that) { + + Comparable thisValue = this.get(property); + Comparable thatValue = that.get(property); + + if(thisValue == null || thatValue == null) { + throw new RuntimeException("Liquid error: comparison of Hash with Hash failed"); + } + + return thisValue.compareTo(thatValue); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + + for(java.util.Map.Entry entry : super.entrySet()) { + builder.append(entry.getKey()).append(entry.getValue()); + } + + return builder.toString(); + } + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Split.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Split.java new file mode 100644 index 00000000000..d62b249e3c0 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Split.java @@ -0,0 +1,23 @@ +package ca.uhn.fhir.narrative.template.filters; + +import java.util.regex.Pattern; + +class Split extends Filter { + + /* + * split(input, delimiter = ' ') + * + * Split a string on a matching pattern + * + * E.g. {{ "a~b" | split:'~' | first }} #=> 'a' + */ + @Override + public Object apply(Object value, Object... params) { + + String original = super.asString(value); + + String delimiter = super.asString(super.get(0, params)); + + return original.split("(?= 1) { + length = super.asNumber(super.get(0, params)).intValue(); + } + + if (params.length >= 2) { + truncateString = super.asString(super.get(1, params)); + } + + if (truncateString.length() >= length) { + return truncateString; + } + + if (length == text.length()) { + return text; + } + + if (length >= (text.length() + truncateString.length())) { + return text; + } + + int remainingChars = length - truncateString.length(); + + return text.substring(0, remainingChars) + truncateString; + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Truncatewords.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Truncatewords.java new file mode 100644 index 00000000000..309700d0d9a --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Truncatewords.java @@ -0,0 +1,47 @@ +package ca.uhn.fhir.narrative.template.filters; + +class Truncatewords extends Filter { + + /* + * truncatewords(input, words = 15, truncate_string = "...") + * + * Truncate a string down to x words + */ + @Override + public Object apply(Object value, Object... params) { + + if (value == null) { + return ""; + } + + String text = super.asString(value); + String[] words = text.split("\\s++"); + int length = 15; + String truncateString = "..."; + + if (params.length >= 1) { + length = super.asNumber(super.get(0, params)).intValue(); + } + + if (params.length >= 2) { + truncateString = super.asString(super.get(1, params)); + } + + if (length >= words.length) { + return text; + } + + return join(words, length) + truncateString; + } + + private String join(String[] words, int length) { + + StringBuilder builder = new StringBuilder(); + + for (int i = 0; i < length; i++) { + builder.append(words[i]).append(" "); + } + + return builder.toString().trim(); + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Upcase.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Upcase.java new file mode 100644 index 00000000000..84770a9e189 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/filters/Upcase.java @@ -0,0 +1,15 @@ +package ca.uhn.fhir.narrative.template.filters; + +class Upcase extends Filter { + + /* + * upcase(input) + * + * convert a input string to UPCASE + */ + @Override + public Object apply(Object value, Object... params) { + + return super.asString(value).toUpperCase(); + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/AndNode.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/AndNode.java new file mode 100644 index 00000000000..43cf01edbb7 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/AndNode.java @@ -0,0 +1,26 @@ +package ca.uhn.fhir.narrative.template.nodes; + +import ca.uhn.fhir.narrative.template.LValue; + +import java.util.Map; + +class AndNode extends LValue implements LNode { + + private LNode lhs; + private LNode rhs; + + public AndNode(LNode lhs, LNode rhs) { + this.lhs = lhs; + this.rhs = rhs; + } + + @Override + public Object render(Map context) { + + Object a = lhs.render(context); + Object b = rhs.render(context); + + return super.asBoolean(a) && super.asBoolean(b); + + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/AtomNode.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/AtomNode.java new file mode 100644 index 00000000000..8539d59dba9 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/AtomNode.java @@ -0,0 +1,24 @@ +package ca.uhn.fhir.narrative.template.nodes; + +import java.util.Map; + +public class AtomNode implements LNode { + + public static final AtomNode EMPTY = new AtomNode(new Object()); + + private Object value; + + public AtomNode(Object value) { + this.value = value; + } + + public static boolean isEmpty(Object o) { + return o == EMPTY.value; + } + + @Override + public Object render(Map context) { + + return value; + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/AttributeNode.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/AttributeNode.java new file mode 100644 index 00000000000..6b55e6aff5b --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/AttributeNode.java @@ -0,0 +1,23 @@ +package ca.uhn.fhir.narrative.template.nodes; + +import java.util.Map; + +class AttributeNode implements LNode { + + private LNode key; + private LNode value; + + public AttributeNode(LNode key, LNode value) { + this.key = key; + this.value = value; + } + + @Override + public Object render(Map context) { + + return new Object[]{ + key.render(context), + value.render(context) + }; + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/BlockNode.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/BlockNode.java new file mode 100644 index 00000000000..813062e68f6 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/BlockNode.java @@ -0,0 +1,58 @@ +package ca.uhn.fhir.narrative.template.nodes; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import ca.uhn.fhir.narrative.template.tags.Tag; + +public class BlockNode implements LNode { + + private List children; + + public BlockNode() { + children = new ArrayList(); + } + + public void add(LNode node) { + children.add(node); + } + + public List getChildren() { + return new ArrayList(children); + } + + @Override + public Object render(Map context) { + + StringBuilder builder = new StringBuilder(); + + for (LNode node : children) { + + Object value = node.render(context); + + if(value == null) { + continue; + } + + if(value == Tag.Statement.BREAK || value == Tag.Statement.CONTINUE) { + return value; + } + + else if (value.getClass().isArray()) { + + Object[] array = (Object[]) value; + + for (Object obj : array) { + builder.append(String.valueOf(obj)); + } + } + else { + builder.append(String.valueOf(value)); + } + } + + return builder.toString(); + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/ContainsNode.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/ContainsNode.java new file mode 100644 index 00000000000..655bea4ea27 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/ContainsNode.java @@ -0,0 +1,34 @@ +package ca.uhn.fhir.narrative.template.nodes; + +import java.util.Arrays; +import java.util.Map; +import ca.uhn.fhir.narrative.template.LValue; + +class ContainsNode extends LValue implements LNode { + + private LNode lhs; + private LNode rhs; + + public ContainsNode(LNode lhs, LNode rhs) { + this.lhs = lhs; + this.rhs = rhs; + } + + @Override + public Object render(Map context) { + + Object collection = lhs.render(context); + Object needle = rhs.render(context); + + if(super.isArray(collection)) { + Object[] array = super.asArray(collection); + return Arrays.asList(array).contains(needle); + } + + if(super.isString(collection)) { + return super.asString(collection).contains(super.asString(needle)); + } + + return false; + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/EqNode.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/EqNode.java new file mode 100644 index 00000000000..d0f6c26bdec --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/EqNode.java @@ -0,0 +1,26 @@ +package ca.uhn.fhir.narrative.template.nodes; + +import ca.uhn.fhir.narrative.template.LValue; + +import java.util.Map; + +class EqNode implements LNode { + + private LNode lhs; + private LNode rhs; + + public EqNode(LNode lhs, LNode rhs) { + this.lhs = lhs; + this.rhs = rhs; + } + + @Override + public Object render(Map context) { + + Object a = lhs.render(context); + Object b = rhs.render(context); + + return LValue.areEqual(a, b); + + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/FilterNode.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/FilterNode.java new file mode 100644 index 00000000000..892dbb09d0f --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/FilterNode.java @@ -0,0 +1,41 @@ +package ca.uhn.fhir.narrative.template.nodes; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import ca.uhn.fhir.narrative.template.filters.Filter; + +public class FilterNode implements LNode { + + private Filter filter; + private List params; + + public FilterNode(String filterName, Filter filter) { + if (filter == null) { + throw new IllegalArgumentException("no filter available named: " + filterName); + } + this.filter = filter; + this.params = new ArrayList(); + } + + public void add(LNode param) { + params.add(param); + } + + public Object apply(Object value, Map variables) { + + List paramValues = new ArrayList(); + + for (LNode node : params) { + paramValues.add(node.render(variables)); + } + + return filter.apply(value, paramValues.toArray(new Object[paramValues.size()])); + } + + @Override + public Object render(Map context) { + throw new RuntimeException("cannot render a filter"); + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/GtEqNode.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/GtEqNode.java new file mode 100644 index 00000000000..c0e185f999b --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/GtEqNode.java @@ -0,0 +1,26 @@ +package ca.uhn.fhir.narrative.template.nodes; + +import ca.uhn.fhir.narrative.template.LValue; + +import java.util.Map; + +class GtEqNode extends LValue implements LNode { + + private LNode lhs; + private LNode rhs; + + public GtEqNode(LNode lhs, LNode rhs) { + this.lhs = lhs; + this.rhs = rhs; + } + + @Override + public Object render(Map context) { + + Object a = lhs.render(context); + Object b = rhs.render(context); + + return (a instanceof Number) && (b instanceof Number) && + super.asNumber(a).doubleValue() >= super.asNumber(b).doubleValue(); + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/GtNode.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/GtNode.java new file mode 100644 index 00000000000..a7c66be8c12 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/GtNode.java @@ -0,0 +1,26 @@ +package ca.uhn.fhir.narrative.template.nodes; + +import ca.uhn.fhir.narrative.template.LValue; + +import java.util.Map; + +class GtNode extends LValue implements LNode { + + private LNode lhs; + private LNode rhs; + + public GtNode(LNode lhs, LNode rhs) { + this.lhs = lhs; + this.rhs = rhs; + } + + @Override + public Object render(Map context) { + + Object a = lhs.render(context); + Object b = rhs.render(context); + + return (a instanceof Number) && (b instanceof Number) && + super.asNumber(a).doubleValue() > super.asNumber(b).doubleValue(); + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/LNode.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/LNode.java new file mode 100644 index 00000000000..50827768f98 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/LNode.java @@ -0,0 +1,21 @@ +package ca.uhn.fhir.narrative.template.nodes; + +import java.util.Map; + +/** + * Denotes a node in the AST the parse creates from the + * input source. + */ +public interface LNode { + + /** + * Renders this AST. + * + * @param context + * the context (variables) with which this + * node should be rendered. + * + * @return an Object denoting the rendered AST. + */ + Object render(Map context); +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/LookupNode.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/LookupNode.java new file mode 100644 index 00000000000..dc2724ccc26 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/LookupNode.java @@ -0,0 +1,73 @@ +package ca.uhn.fhir.narrative.template.nodes; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.hl7.fhir.dstu3.exceptions.FHIRException; +import org.hl7.fhir.dstu3.model.Base; +import org.hl7.fhir.dstu3.model.BooleanType; +import org.hl7.fhir.dstu3.utils.FluentPathEngine; +import org.hl7.fhir.instance.model.api.IBaseBooleanDatatype; +import org.hl7.fhir.instance.model.api.IBaseResource; + +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.util.FhirTerser; + +class LookupNode implements LNode { + + private final String id; + + public LookupNode(String id) { + this.id = id; + } + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(LookupNode.class); + + @Override + public Object render(Map context) { + + ourLog.info("Processing path: {}", id); + + FhirTerser terser = (FhirTerser) context.get("terser"); + FluentPathEngine fpEngine = (FluentPathEngine) context.get("fpEngine"); + IBaseResource resContext = (IBaseResource) context.get("context"); + + List retVal; + try { + retVal = fpEngine.evaluate((Base)resContext, id); + } catch (FHIRException e) { + throw new InternalErrorException(e); + } + // terser.getValues(resContext, id); + + ourLog.info("Evaluated to: {}", retVal); + + if (retVal.size() == 1) { + if (IBaseBooleanDatatype.class.isAssignableFrom(retVal.get(0).getClass())) { + IBaseBooleanDatatype bool = (IBaseBooleanDatatype) retVal.get(0); + Boolean newRetVal = bool.getValue(); + ourLog.info("Returning: {}", newRetVal); + return newRetVal; + } + } + + + return retVal; + } + + interface Indexable { + Object get(Object value, Map context); + } + + @Override + public String toString() { + + StringBuilder builder = new StringBuilder(); + + builder.append(id); + + return builder.toString(); + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/LtEqNode.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/LtEqNode.java new file mode 100644 index 00000000000..a34a7ce4dc7 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/LtEqNode.java @@ -0,0 +1,26 @@ +package ca.uhn.fhir.narrative.template.nodes; + +import ca.uhn.fhir.narrative.template.LValue; + +import java.util.Map; + +class LtEqNode extends LValue implements LNode { + + private LNode lhs; + private LNode rhs; + + public LtEqNode(LNode lhs, LNode rhs) { + this.lhs = lhs; + this.rhs = rhs; + } + + @Override + public Object render(Map context) { + + Object a = lhs.render(context); + Object b = rhs.render(context); + + return (a instanceof Number) && (b instanceof Number) && + super.asNumber(a).doubleValue() <= super.asNumber(b).doubleValue(); + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/LtNode.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/LtNode.java new file mode 100644 index 00000000000..67e0bff283b --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/LtNode.java @@ -0,0 +1,26 @@ +package ca.uhn.fhir.narrative.template.nodes; + +import ca.uhn.fhir.narrative.template.LValue; + +import java.util.Map; + +class LtNode extends LValue implements LNode { + + private LNode lhs; + private LNode rhs; + + public LtNode(LNode lhs, LNode rhs) { + this.lhs = lhs; + this.rhs = rhs; + } + + @Override + public Object render(Map context) { + + Object a = lhs.render(context); + Object b = rhs.render(context); + + return (a instanceof Number) && (b instanceof Number) && + super.asNumber(a).doubleValue() < super.asNumber(b).doubleValue(); + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/NEqNode.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/NEqNode.java new file mode 100644 index 00000000000..3e4c5856d06 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/NEqNode.java @@ -0,0 +1,26 @@ +package ca.uhn.fhir.narrative.template.nodes; + +import ca.uhn.fhir.narrative.template.LValue; + +import java.util.Map; + +class NEqNode implements LNode { + + private LNode lhs; + private LNode rhs; + + public NEqNode(LNode lhs, LNode rhs) { + this.lhs = lhs; + this.rhs = rhs; + } + + @Override + public Object render(Map context) { + + Object a = lhs.render(context); + Object b = rhs.render(context); + + return !LValue.areEqual(a, b); + + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/OrNode.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/OrNode.java new file mode 100644 index 00000000000..663124bbe84 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/OrNode.java @@ -0,0 +1,26 @@ +package ca.uhn.fhir.narrative.template.nodes; + +import ca.uhn.fhir.narrative.template.LValue; + +import java.util.Map; + +class OrNode extends LValue implements LNode { + + private LNode lhs; + private LNode rhs; + + public OrNode(LNode lhs, LNode rhs) { + this.lhs = lhs; + this.rhs = rhs; + } + + @Override + public Object render(Map context) { + + Object a = lhs.render(context); + Object b = rhs.render(context); + + return super.asBoolean(a) || super.asBoolean(b); + + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/OutputNode.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/OutputNode.java new file mode 100644 index 00000000000..95109ee7a57 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/OutputNode.java @@ -0,0 +1,32 @@ +package ca.uhn.fhir.narrative.template.nodes; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +class OutputNode implements LNode { + + private LNode expression; + private List filters; + + public OutputNode(LNode expression) { + this.expression = expression; + this.filters = new ArrayList(); + } + + public void addFilter(FilterNode filter) { + filters.add(filter); + } + + @Override + public Object render(Map context) { + + Object value = expression.render(context); + + for (FilterNode node : filters) { + value = node.apply(value, context); + } + + return value; + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/TagNode.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/TagNode.java new file mode 100644 index 00000000000..bf38008a944 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/nodes/TagNode.java @@ -0,0 +1,40 @@ +package ca.uhn.fhir.narrative.template.nodes; + +import java.io.File; +import java.util.Map; + +import ca.uhn.fhir.narrative.template.parser.Flavor; +import ca.uhn.fhir.narrative.template.tags.Include; +import ca.uhn.fhir.narrative.template.tags.Tag; + +class TagNode implements LNode { + + private Tag tag; + private LNode[] tokens; + private Flavor flavor; + + public TagNode(String tagName, Tag tag, LNode... tokens) { + this(tagName, tag, Flavor.LIQUID, tokens); + } + + public TagNode(String tagName, Tag tag, Flavor flavor, LNode... tokens) { + if (tag == null) { + throw new IllegalArgumentException("no tag available named: " + tagName); + } + this.tag = tag; + this.tokens = tokens; + this.flavor = flavor; + } + + @Override + public Object render(Map context) { + + // Check if the INCLUDES_DIRECTORY_KEY has already been set, and if not, + // set it based on the value in the flavor. + if (!context.containsKey(Include.INCLUDES_DIRECTORY_KEY)) { + context.put(Include.INCLUDES_DIRECTORY_KEY, new File(flavor.snippetsFolderName)); + } + + return tag.render(context, tokens); + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/parser/Flavor.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/parser/Flavor.java new file mode 100644 index 00000000000..fc9161cc845 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/parser/Flavor.java @@ -0,0 +1,13 @@ +package ca.uhn.fhir.narrative.template.parser; + +public enum Flavor { + + LIQUID("snippets"), + JEKYLL("_includes"); + + public final String snippetsFolderName; + + Flavor(String snippetsFolderName) { + this.snippetsFolderName = snippetsFolderName; + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Assign.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Assign.java new file mode 100644 index 00000000000..f289b88afee --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Assign.java @@ -0,0 +1,39 @@ +package ca.uhn.fhir.narrative.template.tags; + +import java.util.Map; + +import ca.uhn.fhir.narrative.template.nodes.FilterNode; +import ca.uhn.fhir.narrative.template.nodes.LNode; + +class Assign extends Tag { + + /* + * Assigns some value to a variable + */ + @Override + public Object render(Map context, LNode... nodes) { + + String id = String.valueOf(nodes[0].render(context)); + + FilterNode filter = null; + LNode expression; + + if(nodes.length >= 3) { + filter = (FilterNode)nodes[1]; + expression = nodes[2]; + } + else { + expression = nodes[1]; + } + + Object value = expression.render(context); + + if(filter != null) { + value = filter.apply(value, context); + } + + context.put(id, value); + + return ""; + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Capture.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Capture.java new file mode 100644 index 00000000000..1ac9f07ed44 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Capture.java @@ -0,0 +1,23 @@ +package ca.uhn.fhir.narrative.template.tags; + +import java.util.Map; + +import ca.uhn.fhir.narrative.template.nodes.LNode; + +class Capture extends Tag { + + /* + * Block tag that captures text into a variable + */ + @Override + public Object render(Map context, LNode... nodes) { + + String id = super.asString(nodes[0].render(context)); + + LNode block = nodes[1]; + + context.put(id, block.render(context)); + + return null; + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Case.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Case.java new file mode 100644 index 00000000000..a7c6eb015be --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Case.java @@ -0,0 +1,60 @@ +package ca.uhn.fhir.narrative.template.tags; + +import ca.uhn.fhir.narrative.template.LValue; + +import java.util.Map; + +import ca.uhn.fhir.narrative.template.nodes.BlockNode; +import ca.uhn.fhir.narrative.template.nodes.LNode; + +class Case extends Tag { + + /* + * Block tag, its the standard case...when block + */ + @Override + public Object render(Map context, LNode... nodes) { + + // ^(CASE condition var + // ^(WHEN term+ block) 1,2,3 b1 + // ^(ELSE block?)) b2 + + Object condition = nodes[0].render(context); + + for (int i = 1; i < nodes.length; i++) { + + LNode node = nodes[i]; + + if(i == nodes.length - 1 && node instanceof BlockNode) { + // this must be the trailing (optional) else-block + return node.render(context); + } + else { + + boolean hit = false; + + // Iterate through the list of terms (of which we do not know the size): + // - term (',' term)* + // - term ('or' term)* + // and stop when we encounter a BlockNode + while(!(node instanceof BlockNode)) { + + Object whenExpressionValue = node.render(context); + + if (LValue.areEqual(condition, whenExpressionValue)) { + hit = true; + } + + i++; + node = nodes[i]; + } + + if(hit) { + return node.render(context); + } + } + } + + return null; + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Comment.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Comment.java new file mode 100644 index 00000000000..0a5a3c6dddb --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Comment.java @@ -0,0 +1,16 @@ +package ca.uhn.fhir.narrative.template.tags; + +import java.util.Map; + +import ca.uhn.fhir.narrative.template.nodes.LNode; + +class Comment extends Tag { + + /* + * Block tag, comments out the text in the block + */ + @Override + public Object render(Map context, LNode... nodes) { + return ""; + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Cycle.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Cycle.java new file mode 100644 index 00000000000..3ad2edeb31c --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Cycle.java @@ -0,0 +1,84 @@ +package ca.uhn.fhir.narrative.template.tags; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import ca.uhn.fhir.narrative.template.nodes.LNode; + +class Cycle extends Tag { + + private static final String PREPEND = "\"'"; + + /* + * Cycle is usually used within a loop to alternate + * between values, like colors or DOM classes. + */ + @Override + public Object render(Map context, LNode... nodes) { + + // The group-name is either the first token-expression, or if that is + // null (indicating there is no name), give it the name PREPEND followed + // by the number of expressions in the cycle-group. + String groupName = nodes[0] == null ? + PREPEND + (nodes.length - 1) : + super.asString(nodes[0].render(context)); + + // Prepend a groupName with a single- and double quote as to not + // let the groupName conflict with other variable assignments + groupName = PREPEND + groupName; + + Object obj = context.remove(groupName); + + List elements = new ArrayList(); + + for (int i = 1; i < nodes.length; i++) { + elements.add(nodes[i].render(context)); + } + + CycleGroup group; + + if (obj == null) { + group = new CycleGroup(elements.size()); + } + else { + group = (CycleGroup) obj; + } + + context.put(groupName, group); + + return group.next(elements); + } + + private static class CycleGroup { + + private final int sizeFirstCycle; + private int currentIndex; + + CycleGroup(int sizeFirstCycle) { + this.sizeFirstCycle = sizeFirstCycle; + this.currentIndex = 0; + } + + Object next(List elements) { + + Object obj; + + if (currentIndex >= elements.size()) { + obj = ""; + } + else { + obj = elements.get(currentIndex); + } + + currentIndex++; + + if (currentIndex == sizeFirstCycle) { + currentIndex = 0; + } + + return obj; + } + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/For.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/For.java new file mode 100644 index 00000000000..de82f2f1256 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/For.java @@ -0,0 +1,253 @@ +package ca.uhn.fhir.narrative.template.tags; + +import java.util.List; +import java.util.HashMap; +import java.util.Map; + +import ca.uhn.fhir.narrative.template.nodes.BlockNode; +import ca.uhn.fhir.narrative.template.nodes.LNode; + +class For extends Tag { + + private static final String OFFSET = "offset"; + private static final String LIMIT = "limit"; + private static final String CONTINUE = "continue"; + + /* + * forloop.length # => length of the entire for loop + * forloop.index # => index of the current iteration + * forloop.index0 # => index of the current iteration (zero based) + * forloop.rindex # => how many items are still left? + * forloop.rindex0 # => how many items are still left? (zero based) + * forloop.first # => is this the first iteration? + * forloop.last # => is this the last iteration? + */ + private static final String FORLOOP = "forloop"; + private static final String LENGTH = "length"; + private static final String INDEX = "index"; + private static final String INDEX0 = "index0"; + private static final String RINDEX = "rindex"; + private static final String RINDEX0 = "rindex0"; + private static final String FIRST = "first"; + private static final String LAST = "last"; + private static final String NAME = "name"; + + /* + * For loop + */ + @Override + public Object render(Map context, LNode... nodes) { + + // The first node in the array denotes whether this is a for-tag + // over an array, `for item in array ...`, or a for-tag over a + // range, `for i in (4..item.length)`. + boolean array = super.asBoolean(nodes[0].render(context)); + + String id = super.asString(nodes[1].render(context)); + + context.put(FORLOOP, new HashMap()); + + Object rendered = array ? renderArray(id, context, nodes) : renderRange(id, context, nodes); + + context.remove(FORLOOP); + + return rendered; + } + + private Object renderArray(String id, Map context, LNode... tokens) { + + StringBuilder builder = new StringBuilder(); + + // attributes start from index 5 + Map attributes = getAttributes(5, context, tokens); + + int offset = attributes.get(OFFSET); + int limit = attributes.get(LIMIT); + + Object[] array = super.asArray(tokens[2].render(context)); + + LNode block = tokens[3]; + LNode blockIfEmptyOrNull = tokens[4]; + + if(array == null || array.length == 0) { + return blockIfEmptyOrNull == null ? null : blockIfEmptyOrNull.render(context); + } + + int length = Math.min(limit, array.length); + + Map forLoopMap = (Map)context.get(FORLOOP); + + forLoopMap.put(NAME, id + "-" + tokens[2].toString()); + + int continueIndex = offset; + + for (int i = offset, n = 0; n < limit && i < array.length; i++, n++) { + + continueIndex = i; + + boolean first = (i == offset); + boolean last = ((n == limit - 1) || (i == array.length - 1)); + + context.put(id, array[i]); + + forLoopMap.put(LENGTH, length); + forLoopMap.put(INDEX, n + 1); + forLoopMap.put(INDEX0, n); + forLoopMap.put(RINDEX, length - n); + forLoopMap.put(RINDEX0, length - n - 1); + forLoopMap.put(FIRST, first); + forLoopMap.put(LAST, last); + + List children = ((BlockNode)block).getChildren(); + boolean isBreak = false; + + for (LNode node : children) { + + Object value = node.render(context); + + if(value == Tag.Statement.CONTINUE) { + // break from this inner loop: equals continue outer loop! + break; + } + + if(value == Tag.Statement.BREAK) { + // break from inner loop + isBreak = true; + break; + } + + if (value != null && value.getClass().isArray()) { + + Object[] arr = (Object[]) value; + + for (Object obj : arr) { + builder.append(String.valueOf(obj)); + } + } + else { + builder.append(super.asString(value)); + } + } + + if(isBreak) { + // break from outer loop + break; + } + } + + context.put(CONTINUE, continueIndex + 1); + + return builder.toString(); + } + + private Object renderRange(String id, Map context, LNode... tokens) { + + StringBuilder builder = new StringBuilder(); + + // attributes start from index 5 + Map attributes = getAttributes(5, context, tokens); + + int offset = attributes.get(OFFSET); + int limit = attributes.get(LIMIT); + + LNode block = tokens[4]; + + try { + int from = super.asNumber(tokens[2].render(context)).intValue(); + int to = super.asNumber(tokens[3].render(context)).intValue(); + + int length = (to - from); + + Map forLoopMap = (Map)context.get(FORLOOP); + + int continueIndex = from + offset; + + for (int i = from + offset, n = 0; i <= to && n < limit; i++, n++) { + + continueIndex = i; + + boolean first = (i == (from + offset)); + boolean last = ((i == to) || (n == limit - 1)); + + context.put(id, i); + + forLoopMap.put(LENGTH, length); + forLoopMap.put(INDEX, n + 1); + forLoopMap.put(INDEX0, n); + forLoopMap.put(RINDEX, length - n); + forLoopMap.put(RINDEX0, length - n - 1); + forLoopMap.put(FIRST, first); + forLoopMap.put(LAST, last); + + List children = ((BlockNode)block).getChildren(); + boolean isBreak = false; + + for (LNode node : children) { + + Object value = node.render(context); + + if(value == null) { + continue; + } + + if(value == Tag.Statement.CONTINUE) { + // break from this inner loop: equals continue outer loop! + break; + } + + if(value == Tag.Statement.BREAK) { + // break from inner loop + isBreak = true; + break; + } + + if(super.isArray(value)) { + + Object[] arr = super.asArray(value); + + for (Object obj : arr) { + builder.append(String.valueOf(obj)); + } + } + else { + builder.append(super.asString(value)); + } + } + + if(isBreak) { + // break from outer loop + break; + } + } + + context.put(CONTINUE, continueIndex + 1); + } + catch (Exception e) { + /* just ignore incorrect expressions */ + } + + return builder.toString(); + } + + private Map getAttributes(int fromIndex, Map context, LNode... tokens) { + + Map attributes = new HashMap(); + + attributes.put(OFFSET, 0); + attributes.put(LIMIT, Integer.MAX_VALUE); + + for (int i = fromIndex; i < tokens.length; i++) { + + Object[] attribute = super.asArray(tokens[i].render(context)); + + try { + attributes.put(super.asString(attribute[0]), super.asNumber(attribute[1]).intValue()); + } + catch (Exception e) { + /* just ignore incorrect attributes */ + } + } + + return attributes; + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/If.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/If.java new file mode 100644 index 00000000000..c469df84ad2 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/If.java @@ -0,0 +1,27 @@ +package ca.uhn.fhir.narrative.template.tags; + +import java.util.Map; + +import ca.uhn.fhir.narrative.template.nodes.LNode; + +class If extends Tag { + + /* + * Standard if/else block + */ + @Override + public Object render(Map context, LNode... nodes) { + + for (int i = 0; i < nodes.length - 1; i += 2) { + + Object exprNodeValue = nodes[i].render(context); + LNode blockNode = nodes[i + 1]; + + if (super.asBoolean(exprNodeValue)) { + return blockNode.render(context); + } + } + + return null; + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Include.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Include.java new file mode 100644 index 00000000000..52e2ea9fef8 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Include.java @@ -0,0 +1,43 @@ +package ca.uhn.fhir.narrative.template.tags; + +import ca.uhn.fhir.narrative.template.Template; + +import java.io.File; +import java.util.Map; + +import ca.uhn.fhir.narrative.template.nodes.LNode; + +public class Include extends Tag { + + public static final String INCLUDES_DIRECTORY_KEY = "liqp@includes_directory"; + public static String DEFAULT_EXTENSION = ".liquid"; + + @Override + public Object render(Map context, LNode... nodes) { + + // This value will always be defined: either a custom file set by the + // user, or else inside TagNode. + File includesDirectory = (File)context.get(INCLUDES_DIRECTORY_KEY); + + try { + String includeResource = super.asString(nodes[0].render(context)); + String extension = DEFAULT_EXTENSION; + if(includeResource.indexOf('.') > 0) { + extension = ""; + } + File includeResourceFile = new File(includesDirectory, includeResource + extension); + Template include = Template.parse(includeResourceFile); + + // check if there's a optional "with expression" + if(nodes.length > 1) { + Object value = nodes[1].render(context); + context.put(includeResource, value); + } + + return include.render(context); + + } catch(Exception e) { + return ""; + } + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Raw.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Raw.java new file mode 100644 index 00000000000..e5c7ad5811a --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Raw.java @@ -0,0 +1,16 @@ +package ca.uhn.fhir.narrative.template.tags; + +import java.util.Map; + +import ca.uhn.fhir.narrative.template.nodes.LNode; + +class Raw extends Tag { + + /* + * temporarily disable tag processing to avoid syntax conflicts. + */ + @Override + public Object render(Map context, LNode... nodes) { + return nodes[0].render(context); + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Tablerow.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Tablerow.java new file mode 100644 index 00000000000..418fc62a11a --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Tablerow.java @@ -0,0 +1,126 @@ +package ca.uhn.fhir.narrative.template.tags; + +import java.util.HashMap; +import java.util.Map; + +import ca.uhn.fhir.narrative.template.nodes.LNode; + +class Tablerow extends Tag { + + private static final String COLS = "cols"; + private static final String LIMIT = "limit"; + + /* + * tablerowloop.length # => length of the entire for loop + * tablerowloop.index # => index of the current iteration + * tablerowloop.index0 # => index of the current iteration (zero based) + * tablerowloop.rindex # => how many items are still left? + * tablerowloop.rindex0 # => how many items are still left? (zero based) + * tablerowloop.first # => is this the first iteration? + * tablerowloop.last # => is this the last iteration? + * tablerowloop.col # => index of column in the current row + * tablerowloop.col0 # => index of column in the current row (zero based) + * tablerowloop.col_first # => is this the first column in the row? + * tablerowloop.col_last # => is this the last column in the row? + */ + private static final String TABLEROWLOOP = "tablerowloop"; + private static final String LENGTH = "length"; + private static final String INDEX = "index"; + private static final String INDEX0 = "index0"; + private static final String RINDEX = "rindex"; + private static final String RINDEX0 = "rindex0"; + private static final String FIRST = "first"; + private static final String LAST = "last"; + private static final String COL = "col"; + private static final String COL0 = "col0"; + private static final String COL_FIRST = "col_first"; + private static final String COL_LAST = "col_last"; + + /* + * Tables + */ + @Override + public Object render(Map context, LNode... nodes) { + + String valueName = super.asString(nodes[0].render(context)); + Object[] collection = super.asArray(nodes[1].render(context)); + LNode block = nodes[2]; + Map attributes = getAttributes(collection, 3, context, nodes); + + int cols = attributes.get(COLS); + int limit = attributes.get(LIMIT); + + Map tablerowloopContext = new HashMap(); + + tablerowloopContext.put(LENGTH, collection.length); + + context.put(TABLEROWLOOP, tablerowloopContext); + + StringBuilder builder = new StringBuilder(); + + int total = Math.min(collection.length, limit); + + if(total == 0) { + + builder.append("\n\n"); + } + else { + + for(int i = 0, c = 1, r = 0; i < total; i++, c++) { + + context.put(valueName, collection[i]); + + tablerowloopContext.put(INDEX0, i); + tablerowloopContext.put(INDEX, i + 1); + tablerowloopContext.put(RINDEX0, total - i - 1); + tablerowloopContext.put(RINDEX, total - i); + tablerowloopContext.put(FIRST, i == 0); + tablerowloopContext.put(LAST, i == total - 1); + tablerowloopContext.put(COL0, c - 1); + tablerowloopContext.put(COL, c); + tablerowloopContext.put(COL_FIRST, c == 1); + tablerowloopContext.put(COL_LAST, c == cols); + + if(c == 1) { + r++; + builder.append("").append(r == 1 ? "\n" : ""); + } + + builder.append(""); + builder.append(super.asString(block.render(context))); + builder.append(""); + + if(c == cols || i == total - 1) { + builder.append("\n"); + c = 0; + } + } + } + + context.remove(TABLEROWLOOP); + + return builder.toString(); + } + + private Map getAttributes(Object[] collection, int fromIndex, Map context, LNode... tokens) { + + Map attributes = new HashMap(); + + attributes.put(COLS, collection.length); + attributes.put(LIMIT, Integer.MAX_VALUE); + + for (int i = fromIndex; i < tokens.length; i++) { + + Object[] attribute = super.asArray(tokens[i].render(context)); + + try { + attributes.put(super.asString(attribute[0]), super.asNumber(attribute[1]).intValue()); + } + catch (Exception e) { + /* just ignore incorrect attributes */ + } + } + + return attributes; + } +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Tag.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Tag.java new file mode 100644 index 00000000000..0ccd3a549e0 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Tag.java @@ -0,0 +1,120 @@ +package ca.uhn.fhir.narrative.template.tags; + +import ca.uhn.fhir.narrative.template.LValue; + +import java.util.HashMap; +import java.util.Map; + +import ca.uhn.fhir.narrative.template.nodes.LNode; + +/** + * Tags are used for the logic in a template. + */ +public abstract class Tag extends LValue { + + public enum Statement { + + BREAK, CONTINUE; + + @Override + public String toString() { + return ""; + } + } + + /** + * A map holding all tags. + */ + private static final Map TAGS = new HashMap(); + + static { + // Register all standard tags. + registerTag(new Assign()); + registerTag(new Case()); + registerTag(new Capture()); + registerTag(new Comment()); + registerTag(new Cycle()); + registerTag(new For()); + registerTag(new If()); + registerTag(new Include()); + registerTag(new Raw()); + registerTag(new Tablerow()); + registerTag(new Unless()); + } + + /** + * The name of this tag. + */ + public final String name; + + /** + * Used for all package protected tags in the ca.uhn.fhir.narrative.template.tags-package + * whose name is their class name lower cased. + */ + protected Tag() { + this.name = this.getClass().getSimpleName().toLowerCase(); + } + + /** + * Creates a new instance of a Tag. + * + * @param name + * the name of the tag. + */ + public Tag(String name) { + this.name = name; + } + + /** + * Retrieves a filter with a specific name. + * + * @param name + * the name of the filter to retrieve. + * + * @return a filter with a specific name. + */ + public static Tag getTag(String name) { + + Tag tag = TAGS.get(name); + + if (tag == null) { + throw new RuntimeException("unknown tag: " + name); + } + + return tag; + } + + /** + * Returns all default tags. + * + * @return all default tags. + */ + public static Map getTags() { + return new HashMap(TAGS); + } + + /** + * Registers a new tag. + * + * @param tag + * the tag to be registered. + */ + public static void registerTag(Tag tag) { + TAGS.put(tag.name, tag); + } + + /** + * Renders this tag. + * + * @param context + * the context (variables) with which this + * node should be rendered. + * @param nodes + * the nodes of this tag is created with. See + * the file `src/grammar/LiquidWalker.g` to see + * how each of the tags is created. + * + * @return an Object denoting the rendered AST. + */ + public abstract Object render(Map context, LNode... nodes); +} diff --git a/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Unless.java b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Unless.java new file mode 100644 index 00000000000..5c9acaab0f9 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/main/java/ca/uhn/fhir/narrative/template/tags/Unless.java @@ -0,0 +1,27 @@ +package ca.uhn.fhir.narrative.template.tags; + +import java.util.Map; + +import ca.uhn.fhir.narrative.template.nodes.LNode; + +class Unless extends Tag { + + /* + * Mirror of if statement + */ + @Override + public Object render(Map context, LNode... nodes) { + + for (int i = 0; i < nodes.length - 1; i += 2) { + + Object exprNodeValue = nodes[i].render(context); + LNode blockNode = nodes[i + 1]; + + if (!super.asBoolean(exprNodeValue)) { + return blockNode.render(context); + } + } + + return ""; + } +} diff --git a/hapi-fhir-narrativegenerator/src/test/java/ca/uhn/fhir/narrative/template/TemplateNarrativeGeneratorTest.java b/hapi-fhir-narrativegenerator/src/test/java/ca/uhn/fhir/narrative/template/TemplateNarrativeGeneratorTest.java new file mode 100644 index 00000000000..b50f058e0b0 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/test/java/ca/uhn/fhir/narrative/template/TemplateNarrativeGeneratorTest.java @@ -0,0 +1,32 @@ + +package ca.uhn.fhir.narrative.template; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.apache.commons.io.IOUtils; +import org.hl7.fhir.dstu3.model.Patient; +import org.junit.Test; + +import ca.uhn.fhir.context.FhirContext; + +public class TemplateNarrativeGeneratorTest { + + @Test + public void testTemplate() throws Exception { + String template = IOUtils.toString(getClass().getResourceAsStream("/patient.narrative"), StandardCharsets.UTF_8); + + Patient input = new Patient(); + input.addName().addFamily("LNAME1"); + input.addName().addFamily("LNAME2"); + input.addName().addGiven("FNAME1"); + input.addName().addGiven("FNAME2"); + + TemplateNarrativeGenerator gen = new TemplateNarrativeGenerator(); + String output = gen.processLiquid(FhirContext.forDstu3(), template, input); + + ourLog.info(output); + } + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TemplateNarrativeGeneratorTest.class); +} diff --git a/hapi-fhir-narrativegenerator/src/test/resources/patient.narrative b/hapi-fhir-narrativegenerator/src/test/resources/patient.narrative new file mode 100644 index 00000000000..eaa90a6c124 --- /dev/null +++ b/hapi-fhir-narrativegenerator/src/test/resources/patient.narrative @@ -0,0 +1,8 @@ +
+ + {% if name.empty() == false %} + {{ name[0].family }} + {{ name[0].given }} + {% endif %} + +
\ No newline at end of file diff --git a/pom.xml b/pom.xml index 5360e502cf1..0aaeca1fa9f 100644 --- a/pom.xml +++ b/pom.xml @@ -49,6 +49,10 @@ bintray http://dl.bintray.com/dnault/maven + + jitpack.io + https://jitpack.io + @@ -315,6 +319,11 @@ logback-classic 1.1.7 + + com.github.bkiers + Liqp + 0.6.4 + com.github.dnault xml-patch