Merge branch 'narrative_generator'

This commit is contained in:
James Agnew 2016-11-21 16:04:27 +01:00
commit 2d52affd8d
73 changed files with 4201 additions and 0 deletions

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" path="target/generated-sources/antlr3"/>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="org.eclipse.jst.component.nondependency" value=""/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@ -0,0 +1,2 @@
eclipse.preferences.version=1
encoding/<project>=UTF-8

View File

@ -0,0 +1,4 @@
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1

View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>2.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
<artifactId>hapi-fhir-hapi-fhir-narrativegenerator</artifactId>
<packaging>jar</packaging>
<name>HAPI FHIR - Narrative Generator</name>
<dependencies>
<!-- HAPI DEPENDENCIES -->
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-base</artifactId>
<version>2.1-SNAPSHOT</version>
</dependency>
<!-- conformance profile -->
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-dstu2</artifactId>
<version>2.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-dstu3</artifactId>
<version>2.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.antlr</groupId>
<artifactId>antlr</artifactId>
<version>3.5.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.antlr</groupId>
<artifactId>antlr3-maven-plugin</artifactId>
<version>3.5.2</version>
<executions>
<execution>
<goals>
<goal>antlr</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<configuration>
<classFolders>
<classFolder>${basedir}/target/classes</classFolder>
<classFolder>${basedir}/../hapi-fhir-base/target/classes</classFolder>
</classFolders>
<sourceFolders>
<sourceFolder>${basedir}/src/main/java</sourceFolder>
<sourceFolder>${basedir}/../hapi-fhir-base/src/main/java</sourceFolder>
</sourceFolders>
<dumpOnExit>true</dumpOnExit>
</configuration>
<executions>
<execution>
<id>default-prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -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<String, Tag> tags;
private Map<String, Filter> filters;
private Flavor flavor;
public LiquidWalker(TreeNodeStream nodes, Map<String, Tag> tags, Map<String, Filter> filters) {
this(nodes, tags, filters, Flavor.LIQUID);
}
public LiquidWalker(TreeNodeStream nodes, Map<String, Tag> tags, Map<String, Filter> 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<LNode> nodes = new ArrayList<LNode>();}
: ^(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<LNode> nodes = new ArrayList<LNode>();}
: ^(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<LNode> nodes = new ArrayList<LNode>();}
: ^(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<LNode> nodes]
: ^(WHEN (expr {nodes.add($expr.node);})+ block) {nodes.add($block.node);}
;
cycle_tag returns [LNode node]
@init{List<LNode> nodes = new ArrayList<LNode>();}
: ^(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<LNode> expressions = new ArrayList<LNode>();
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<LNode> expressions = new ArrayList<LNode>();
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<LNode> expressions = new ArrayList<LNode>();
}
: ^(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<LNode> expressions = new ArrayList<LNode>();}
: ^(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<LNode> expressions = new ArrayList<LNode>();}
: ^(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);}
)
;

View File

@ -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';

View File

@ -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.
* <p/>
* 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;
}
}

View File

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

View File

@ -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<String, Object> context = new HashMap<String, Object>();
context.put("context", theResource);
context.put("terser", terser);
context.put("fpEngine", fluentPathEngine);
return template.render(context);
}
class MyContextMap extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
private List<Base> myContext;
private IBaseResource myResource;
MyContextMap(IBaseResource theResource) {
myResource = theResource;
myContext = null;
}
MyContextMap(Base theContext, IBaseResource theResource) {
myContext = Collections.singletonList(theContext);
myResource=theResource;
}
MyContextMap(List<Base> 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<Base> evaluated = new ArrayList<Base>();
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);
}
}
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}

View File

@ -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<String> datePatterns = new HashSet<String>();
private final static java.util.Map<Character, SimpleDateFormat> LIQUID_TO_JAVA_FORMAT =
new java.util.HashMap<Character, SimpleDateFormat>();
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;
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;");
}
}

View File

@ -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]+);)", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;");
}
}

View File

@ -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.
* <p/>
* -- https://github.com/Shopify/liquid/wiki/Liquid-for-Designers
*/
public abstract class Filter extends LValue {
/**
* A map holding all filters.
*/
private static final Map<String, Filter> FILTERS = new HashMap<String, Filter>();
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<String, Filter> getFilters() {
return new HashMap<String, Filter>(FILTERS);
}
/**
* Registers a new filter.
*
* @param filter
* the filter to be registered.
*/
public static void registerFilter(Filter filter) {
FILTERS.put(filter.name, filter);
}
}

View File

@ -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]);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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]);
}
}

View File

@ -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<Object> list = new ArrayList<Object>();
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()]);
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,15 @@
package ca.uhn.fhir.narrative.template.filters;
class Newline_To_Br extends Filter {
/*
* newline_to_br(input)
*
* Add <br /> tags in front of all newlines in input string
*/
@Override
public Object apply(Object value, Object... params) {
return super.asString(value).replaceAll("[\n]", "<br />\n");
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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), "");
}
}

View File

@ -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)), "");
}
}

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -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;
}
}

View File

@ -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<Comparable> list = asComparableList(array, property);
Collections.sort(list);
return property == null ?
list.toArray(new Comparable[list.size()]) :
list.toArray(new SortableMap[list.size()]);
}
private List<Comparable> asComparableList(Object[] array, String property) {
List<Comparable> list = new ArrayList<Comparable>();
for (Object obj : array) {
if(obj instanceof java.util.Map && property != null) {
list.add(new SortableMap((java.util.Map<String, Comparable>)obj, property));
}
else {
list.add((Comparable) obj);
}
}
return list;
}
static class SortableMap extends HashMap<String, Comparable> implements Comparable<SortableMap> {
final String property;
SortableMap(java.util.Map<String, Comparable> 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();
}
}
}

View File

@ -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("(?<!^)" + Pattern.quote(delimiter));
}
}

View File

@ -0,0 +1,18 @@
package ca.uhn.fhir.narrative.template.filters;
class Strip_HTML extends Filter {
/*
* strip_html(input)
*
* Remove all HTML tags from the string
*/
@Override
public Object apply(Object value, Object... params) {
throw new UnsupportedOperationException();
//
// String html = super.asString(value);
//
// return Jsoup.parse(html).text();
}
}

View File

@ -0,0 +1,15 @@
package ca.uhn.fhir.narrative.template.filters;
class Strip_Newlines extends Filter {
/*
* strip_newlines(input) click to toggle source
*
* Remove all newlines from the string
*/
@Override
public Object apply(Object value, Object... params) {
return super.asString(value).replaceAll("[\r\n]++", "");
}
}

View File

@ -0,0 +1,27 @@
package ca.uhn.fhir.narrative.template.filters;
class Times extends Filter {
/*
* times(input, operand)
*
* multiplication
*/
@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();
}
}

View File

@ -0,0 +1,45 @@
package ca.uhn.fhir.narrative.template.filters;
class Truncate extends Filter {
/*
* truncate(input, length = 50, truncate_string = "...")
*
* Truncate a string down to x characters
*/
@Override
public Object apply(Object value, Object... params) {
if (value == null) {
return "";
}
String text = super.asString(value);
int length = 50;
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 (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;
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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<String, Object> context) {
Object a = lhs.render(context);
Object b = rhs.render(context);
return super.asBoolean(a) && super.asBoolean(b);
}
}

View File

@ -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<String, Object> context) {
return value;
}
}

View File

@ -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<String, Object> context) {
return new Object[]{
key.render(context),
value.render(context)
};
}
}

View File

@ -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<LNode> children;
public BlockNode() {
children = new ArrayList<LNode>();
}
public void add(LNode node) {
children.add(node);
}
public List<LNode> getChildren() {
return new ArrayList<LNode>(children);
}
@Override
public Object render(Map<String, Object> 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();
}
}

View File

@ -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<String, Object> 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;
}
}

View File

@ -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<String, Object> context) {
Object a = lhs.render(context);
Object b = rhs.render(context);
return LValue.areEqual(a, b);
}
}

View File

@ -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<LNode> 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<LNode>();
}
public void add(LNode param) {
params.add(param);
}
public Object apply(Object value, Map<String, Object> variables) {
List<Object> paramValues = new ArrayList<Object>();
for (LNode node : params) {
paramValues.add(node.render(variables));
}
return filter.apply(value, paramValues.toArray(new Object[paramValues.size()]));
}
@Override
public Object render(Map<String, Object> context) {
throw new RuntimeException("cannot render a filter");
}
}

View File

@ -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<String, Object> 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();
}
}

View File

@ -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<String, Object> 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();
}
}

View File

@ -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<String, Object> context);
}

View File

@ -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<String, Object> 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<Base> 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<String, Object> context);
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append(id);
return builder.toString();
}
}

View File

@ -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<String, Object> 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();
}
}

View File

@ -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<String, Object> 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();
}
}

View File

@ -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<String, Object> context) {
Object a = lhs.render(context);
Object b = rhs.render(context);
return !LValue.areEqual(a, b);
}
}

View File

@ -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<String, Object> context) {
Object a = lhs.render(context);
Object b = rhs.render(context);
return super.asBoolean(a) || super.asBoolean(b);
}
}

View File

@ -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<FilterNode> filters;
public OutputNode(LNode expression) {
this.expression = expression;
this.filters = new ArrayList<FilterNode>();
}
public void addFilter(FilterNode filter) {
filters.add(filter);
}
@Override
public Object render(Map<String, Object> context) {
Object value = expression.render(context);
for (FilterNode node : filters) {
value = node.apply(value, context);
}
return value;
}
}

View File

@ -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<String, Object> 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);
}
}

View File

@ -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;
}
}

View File

@ -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<String, Object> 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 "";
}
}

View File

@ -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<String, Object> context, LNode... nodes) {
String id = super.asString(nodes[0].render(context));
LNode block = nodes[1];
context.put(id, block.render(context));
return null;
}
}

View File

@ -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<String, Object> 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;
}
}

View File

@ -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<String, Object> context, LNode... nodes) {
return "";
}
}

View File

@ -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<String, Object> 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<Object> elements = new ArrayList<Object>();
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<Object> elements) {
Object obj;
if (currentIndex >= elements.size()) {
obj = "";
}
else {
obj = elements.get(currentIndex);
}
currentIndex++;
if (currentIndex == sizeFirstCycle) {
currentIndex = 0;
}
return obj;
}
}
}

View File

@ -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<String, Object> 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<String, Object>());
Object rendered = array ? renderArray(id, context, nodes) : renderRange(id, context, nodes);
context.remove(FORLOOP);
return rendered;
}
private Object renderArray(String id, Map<String, Object> context, LNode... tokens) {
StringBuilder builder = new StringBuilder();
// attributes start from index 5
Map<String, Integer> 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<String, Object> forLoopMap = (Map<String, Object>)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<LNode> 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<String, Object> context, LNode... tokens) {
StringBuilder builder = new StringBuilder();
// attributes start from index 5
Map<String, Integer> 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<String, Object> forLoopMap = (Map<String, Object>)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<LNode> 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<String, Integer> getAttributes(int fromIndex, Map<String, Object> context, LNode... tokens) {
Map<String, Integer> attributes = new HashMap<String, Integer>();
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;
}
}

View File

@ -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<String, Object> 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;
}
}

View File

@ -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<String, Object> 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 "";
}
}
}

View File

@ -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<String, Object> context, LNode... nodes) {
return nodes[0].render(context);
}
}

View File

@ -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<String, Object> context, LNode... nodes) {
String valueName = super.asString(nodes[0].render(context));
Object[] collection = super.asArray(nodes[1].render(context));
LNode block = nodes[2];
Map<String, Integer> attributes = getAttributes(collection, 3, context, nodes);
int cols = attributes.get(COLS);
int limit = attributes.get(LIMIT);
Map<String, Object> tablerowloopContext = new HashMap<String, Object>();
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("<tr class=\"row1\">\n</tr>\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("<tr class=\"row").append(r).append("\">").append(r == 1 ? "\n" : "");
}
builder.append("<td class=\"col").append(c).append("\">");
builder.append(super.asString(block.render(context)));
builder.append("</td>");
if(c == cols || i == total - 1) {
builder.append("</tr>\n");
c = 0;
}
}
}
context.remove(TABLEROWLOOP);
return builder.toString();
}
private Map<String, Integer> getAttributes(Object[] collection, int fromIndex, Map<String, Object> context, LNode... tokens) {
Map<String, Integer> attributes = new HashMap<String, Integer>();
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;
}
}

View File

@ -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<String, Tag> TAGS = new HashMap<String, Tag>();
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<String, Tag> getTags() {
return new HashMap<String, Tag>(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<String, Object> context, LNode... nodes);
}

View File

@ -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<String, Object> 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 "";
}
}

View File

@ -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);
}

View File

@ -0,0 +1,8 @@
<div>
{% if name.empty() == false %}
{{ name[0].family }}
{{ name[0].given }}
{% endif %}
</div>

View File

@ -49,6 +49,10 @@
<name>bintray</name>
<url>http://dl.bintray.com/dnault/maven</url>
</repository>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<description>
@ -327,6 +331,11 @@
<artifactId>logback-classic</artifactId>
<version>1.1.7</version>
</dependency>
<dependency>
<groupId>com.github.bkiers</groupId>
<artifactId>Liqp</artifactId>
<version>0.6.4</version>
</dependency>
<dependency>
<groupId>com.github.dnault</groupId>
<artifactId>xml-patch</artifactId>