Skip to content

Commit 0368d84

Browse files
committed
Add support for conditional sections.
1 parent 140e044 commit 0368d84

File tree

5 files changed

+96
-65
lines changed

5 files changed

+96
-65
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
[![Maven Central](https://img.shields.io/maven-central/v/org.httprpc/httprpc-server.svg)](https://repo1.maven.org/maven2/org/httprpc/httprpc-server/)
33

44
# Introduction
5-
HTTP-RPC is an open-source framework for creating and consuming RESTful and REST-like web services in Java. It is extremely lightweight and requires only a Java runtime environment and a servlet container. The entire framework is less than 100KB in size, making it an ideal choice for applications where a minimal footprint is desired.
5+
HTTP-RPC is an open-source framework for creating and consuming RESTful and REST-like web services in Java. It is extremely lightweight and requires only a Java runtime environment and a servlet container. The entire framework is less than 90KB in size, making it an ideal choice for applications where a minimal footprint is desired.
66

77
This guide introduces the HTTP-RPC framework and provides an overview of its key features.
88

httprpc-client/src/main/java/org/httprpc/io/TemplateEncoder.java

Lines changed: 30 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
import java.util.Date;
3939
import java.util.HashMap;
4040
import java.util.Iterator;
41-
import java.util.LinkedList;
4241
import java.util.Locale;
4342
import java.util.Map;
4443
import java.util.ResourceBundle;
@@ -342,7 +341,8 @@ public Object apply(Object value, String argument, Locale locale, TimeZone timeZ
342341

343342
// Marker type enumeration
344343
private enum MarkerType {
345-
SECTION_START,
344+
REPEATING_SECTION_START,
345+
CONDITIONAL_SECTION_START,
346346
SECTION_END,
347347
INCLUDE,
348348
COMMENT,
@@ -404,9 +404,6 @@ public Set<Entry<Object, Object>> entrySet() {
404404
private String baseName = null;
405405
private Map<String, ?> context = emptyMap();
406406

407-
private Map<String, Reader> includes = new HashMap<>();
408-
private LinkedList<Map<String, Reader>> history = new LinkedList<>();
409-
410407
private static HashMap<String, Modifier> defaultModifiers = new HashMap<>();
411408

412409
private static final int EOF = -1;
@@ -668,8 +665,10 @@ private void writeRoot(Object root, Writer writer, Locale locale, TimeZone timeZ
668665
c = reader.read();
669666

670667
MarkerType markerType;
671-
if (c == '#') {
672-
markerType = MarkerType.SECTION_START;
668+
if (c == '?') {
669+
markerType = MarkerType.CONDITIONAL_SECTION_START;
670+
} else if (c == '#') {
671+
markerType = MarkerType.REPEATING_SECTION_START;
673672
} else if (c == '/') {
674673
markerType = MarkerType.SECTION_END;
675674
} else if (c == '>') {
@@ -709,7 +708,7 @@ private void writeRoot(Object root, Writer writer, Locale locale, TimeZone timeZ
709708
}
710709

711710
switch (markerType) {
712-
case SECTION_START: {
711+
case REPEATING_SECTION_START: {
713712
String separator = null;
714713

715714
int n = marker.length();
@@ -724,9 +723,12 @@ private void writeRoot(Object root, Writer writer, Locale locale, TimeZone timeZ
724723
}
725724
}
726725

727-
history.push(includes);
728-
729-
Object value = dictionary.get(marker);
726+
Object value;
727+
if (marker.equals(".")) {
728+
value = dictionary.get(marker);
729+
} else {
730+
value = BeanAdapter.valueAt(dictionary, marker);
731+
}
730732

731733
Iterator<?> iterator;
732734
if (value == null) {
@@ -740,8 +742,6 @@ private void writeRoot(Object root, Writer writer, Locale locale, TimeZone timeZ
740742
}
741743

742744
if (iterator.hasNext()) {
743-
includes = new HashMap<>();
744-
745745
int i = 0;
746746

747747
while (iterator.hasNext()) {
@@ -764,22 +764,25 @@ private void writeRoot(Object root, Writer writer, Locale locale, TimeZone timeZ
764764
i++;
765765
}
766766
} else {
767-
includes = new AbstractMap<String, Reader>() {
768-
@Override
769-
public Reader get(Object key) {
770-
return new EmptyReader();
771-
}
767+
writeRoot(null, new NullWriter(), locale, timeZone, reader);
768+
}
772769

773-
@Override
774-
public Set<Entry<String, Reader>> entrySet() {
775-
throw new UnsupportedOperationException();
776-
}
777-
};
770+
break;
771+
}
778772

779-
writeRoot(emptyMap(), new NullWriter(), locale, timeZone, reader);
773+
case CONDITIONAL_SECTION_START: {
774+
Object value;
775+
if (marker.equals(".")) {
776+
value = dictionary.get(marker);
777+
} else {
778+
value = BeanAdapter.valueAt(dictionary, marker);
780779
}
781780

782-
includes = history.pop();
781+
if (value != null) {
782+
writeRoot(value, writer, locale, timeZone, reader);
783+
} else {
784+
writeRoot(null, new NullWriter(), locale, timeZone, reader);
785+
}
783786

784787
break;
785788
}
@@ -790,22 +793,12 @@ public Set<Entry<String, Reader>> entrySet() {
790793
}
791794

792795
case INCLUDE: {
793-
Reader include = includes.get(marker);
794-
795-
if (include == null) {
796+
if (root != null) {
796797
URL url = new URL(this.url, marker);
797798

798799
try (InputStream inputStream = url.openStream()) {
799-
include = new PagedReader(new InputStreamReader(inputStream));
800-
801-
writeRoot(dictionary, writer, locale, timeZone, include);
802-
803-
includes.put(marker, include);
800+
writeRoot(dictionary, writer, locale, timeZone, new PagedReader(new InputStreamReader(inputStream)));
804801
}
805-
} else {
806-
include.reset();
807-
808-
writeRoot(dictionary, writer, locale, timeZone, include);
809802
}
810803

811804
break;

httprpc-client/src/test/java/org/httprpc/io/TemplateEncoderTest.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,24 @@ public void testMapSection2() throws IOException {
268268
assertEquals("a:A,b:B,c:C", result);
269269
}
270270

271+
@Test
272+
public void testConditionalSection() throws IOException {
273+
TemplateEncoder encoder = new TemplateEncoder(getClass().getResource("section8.txt"));
274+
275+
Map<String, ?> value = mapOf(
276+
entry("a", "A"),
277+
entry("b", "B")
278+
);
279+
280+
String result;
281+
try (StringWriter writer = new StringWriter()) {
282+
encoder.write(value, writer);
283+
result = writer.toString();
284+
}
285+
286+
assertEquals("12", result);
287+
}
288+
271289
@Test
272290
public void testComment() throws IOException {
273291
TemplateEncoder encoder = new TemplateEncoder(getClass().getResource("comment.txt"));
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{{?a}}1{{/a}}{{?b}}2{{/b}}{{?c}}3{{/c}}

template-reference.md

Lines changed: 46 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ Templates are documents that describe an output format such as HTML. They allow
44
Template documents include "markers" that are replaced with values provided by the data dictionary when the template is processed:
55

66
* {{_variable_}} - injects a variable from the data dictionary into the output
7-
* {{#_section_}}...{{/_section_}} - defines a repeating section of content
7+
* {{#_section_}}...{{/_section_}} - defines a repeating section
8+
* {{?_section_}}...{{/_section_}} - defines a conditional section
89
* {{>_include_}} - imports content from another template
9-
* {{!_comment_}} - provides informational text about a template's content
10+
* {{!_comment_}} - provides non-rendered informational content
1011

1112
Each of these marker types is discussed in more detail below.
1213

13-
## Variable Markers
14+
## Variables
1415
Variable markers inject a value from the data dictionary into the output. For example:
1516

1617
```html
@@ -19,13 +20,11 @@ Variable markers inject a value from the data dictionary into the output. For ex
1920
<p>Average: {{average}}</p>
2021
```
2122

22-
Variable names represent keys into the data dictionary. When the template is processed, the markers are replaced with the corresponding values from the dictionary.
23+
Variable names represent keys into the data dictionary. When the template is processed, the markers are replaced with the corresponding values from the dictionary. If a variable value is not defined (i.e. is missing or `null`), it is excluded from the generated output.
2324

24-
Nested values can be referred to using path notation; e.g. "name.first". Missing (i.e. `null`) values are replaced with the empty string in the generated output.
25+
Nested values can be referred to using path notation; e.g. "name.first". Variable names beginning with "@" represent resource references and are obtained from the resource bundle when the template is processed. Variable names beginning with "$" represent context references and are obtained from the template context.
2526

26-
Variable names beginning with "@" represent resource references, and are replaced with the corresponding values from the resource bundle when the template is processed.
27-
28-
Variable names beginning with "$" represent context references, and are replaced with the corresponding values from the template context when the template is processed.
27+
The reserved "~" and "." variable names represent key and value references, respectively, and are discussed in more detail below.
2928

3029
### Modifiers
3130
Modifiers are used to transform a variable's representation before it is written to the output stream; for example, to apply an escape sequence.
@@ -86,41 +85,39 @@ Date/time values may be represented by one of the following:
8685
* an instance of `java.util.Date`
8786
* an instance of `java.util.time.TemporalAccessor`
8887

89-
## Section Markers
90-
Section markers define a repeating section of content. The marker name must refer to a traversable sequence of elements in the data dictionary (specifically, an instance of either `java.lang.Iterable` or `java.util.Map`). Missing or empty sequences are ignored.
91-
92-
Content between the markers is repeated once for each element in the sequence. The element provides the data dictionary for each successive iteration through the section.
88+
## Repeating Sections
89+
Repeating section markers define a section of content that is repeated once for every element in a sequence of values. The marker name must refer to an instance of either `java.lang.Iterable` or `java.util.Map` in the data dictionary. The elements of the sequence provide the data dictionaries for successive iterations through the section. Missing or empty sequences are ignored.
9390

9491
For example, a data dictionary that contains information about homes for sale might look like this:
9592

9693
```json
9794
{
9895
"properties": [
9996
{
100-
"streetAddress": "17 Cardinal St.",
101-
"listPrice": 849000,
97+
"streetAddress": "27 Crescent St.",
98+
"listPrice": 925000,
10299
"numberOfBedrooms": 4,
103100
"numberOfBathrooms": 3
104101
},
105102
{
106-
"streetAddress": "72 Wedgemere Ave.",
107-
"listPrice": 1650000,
108-
"numberOfBedrooms": 5,
109-
"numberOfBathrooms": 3
103+
"streetAddress": "390 North Elm St.",
104+
"listPrice": 7650000,
105+
"numberOfBedrooms": 3,
106+
"numberOfBathrooms": 1.5
110107
},
111108
...
112109
]
113110
}
114111
```
115112

116-
A template to transform these results into HTML is shown below. The section markers are enclosed in HTML comments so they will be ignored by syntax-aware text editors, and will simply resolve to empty comment blocks when the template is processed. The HTML encoding modifier is applied to the string values to ensure that the generated output is properly escaped:
113+
A template to transform these results into HTML is shown below. The section markers are enclosed in HTML comments so they will be ignored by syntax-aware text editors, and will simply resolve to empty comment blocks when the template is processed:
117114

118115
```html
119116
<table>
120117
<!-- {{#properties}} -->
121118
<tr>
122-
<td>{{streetAddress:^html}}</td>
123-
<td>{{listPrice:format=currency:^html}}</td>
119+
<td>{{streetAddress}}</td>
120+
<td>{{listPrice:format=currency}}</td>
124121
<td>{{numberOfBedrooms}}</td>
125122
<td>{{numberOfBathrooms}}</td>
126123
</tr>
@@ -131,18 +128,20 @@ A template to transform these results into HTML is shown below. The section mark
131128
### Separators
132129
Section markers may specify an optional separator string that will be automatically injected between the section's elements. The separator text is enclosed in square brackets immediately following the section name.
133130

134-
For example, the elements of the "addresses" section specified below will be separated by a comma in the generated output:
131+
For example, the elements of the "names" section specified below will be separated by a comma in the generated output:
135132

136133
```
137-
{{#addresses[,]}}
134+
{{#names[,]}}
138135
...
139-
{{/addresses}}
136+
{{/names}}
140137
```
141138

142139
### Key and Value References
143-
When traversing the contents of a `Map` instance, the "~" variable can be used to refer to the key associated with the current element. Additionally, when a sequence element is not an instance of `Map` (for example, a `Number`, `String`, or `Iterable`), the "." variable can be used to refer to the value of the element itself.
140+
In most cases, variable names are used to refer to properties of the `Map` instance representing the current data dictionary. However, when traversing the contents of a `Map` sequence, the reserved "~" variable can be used to refer to the key associated with the current element.
141+
142+
Additionally, when traversing any type of sequence (`Iterable` or `Map`), if the current element is not a `Map` (for example, a `Number`, `String`, or `Iterable`), the "." variable can be used to refer to the value of the element itself.
144143

145-
For example, the following data dictionary associates number names with numeric values:
144+
For example, the following data dictionary associates a set of number names with their corresponding numeric values:
146145

147146
```json
148147
{
@@ -154,12 +153,32 @@ For example, the following data dictionary associates number names with numeric
154153
}
155154
```
156155

157-
This simple template could be used to generate a comma-separated list of name/value pairs from the data dictionary:
156+
This template could be used to generate a comma-separated list of name/value pairs from the data dictionary:
158157

159158
```
160159
{{#numbers[,]}}{{~}}:{{.}}{{/numbers}}
161160
```
162161

162+
## Conditional Sections
163+
Conditional section markers define a section of content that is only rendered if the named value exists in the data dictionary. When the value exists, it is used as the data dictionary for the section. If the value does not exist or is `null`, the section is excluded from the output.
164+
165+
For example, given the following data dictionary:
166+
167+
```json
168+
{
169+
"name": {
170+
"first": "John",
171+
"last": "Smith"
172+
}
173+
}
174+
```
175+
176+
the content of "name" section in the following template would be included in the generated output, but the content of the "age" section would not:
177+
178+
```
179+
{{?name}}{{last}}, {{first}}{{/name}}{{?age}}, age {{.}}{{/age}}
180+
```
181+
163182
## Includes
164183
Include markers import content defined by another template. They can be used to create reusable content modules; for example, document headers and footers.
165184

0 commit comments

Comments
 (0)