Skip to content
This repository has been archived by the owner on Jan 8, 2020. It is now read-only.

Commit

Permalink
Improved "object builder" syntax used for configuring FreeMarker from…
Browse files Browse the repository at this point in the history
… java.util.Properties (or other string-only sources): Added support for creating maps with { key1: value1, key2: value2, ... keyN: valueN } syntax.
  • Loading branch information
ddekany committed Sep 1, 2015
1 parent ed1bf24 commit 1b429d8
Show file tree
Hide file tree
Showing 4 changed files with 228 additions and 4 deletions.
7 changes: 6 additions & 1 deletion src/main/java/freemarker/core/Configurable.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
Expand Down Expand Up @@ -1796,9 +1797,13 @@ public boolean isLogTemplateExceptionsSet() {
* <li>The null literal: {@code null}
* <li>A string literal with FTL syntax, except that it can't contain <tt>${...}</tt>-s and
* <tt>#{...}</tt>-s. Examples: {@code "Line 1\nLine 2"} or {@code r"C:\temp"}.
* <li>A list literal (since 2.3.24) with FTL-like syntax, for example {@code ['foo', 2, true]}.
* <li>A list literal (since 2.3.24) with FTL-like syntax, for example {@code [ 'foo', 2, true ]}.
* If the parameter is expected to be array, the list will be automatically converted to array.
* The list items can be any kind of expression, like even object builder expressions.
* <li>A map literal (since 2.3.24) with FTL-like syntax, for example <code>{ 'foo': 2, 'bar': true }</code>.
* The keys and values can be any kind of expression, like even object builder expressions.
* The resulting Java object will be a {@link Map} that keeps the item order ({@link LinkedHashMap} as
* of this writing).
* <li>An object builder expression. That is, object builder expressions can be nested into each other.
* </ul>
* </li>
Expand Down
76 changes: 76 additions & 0 deletions src/main/java/freemarker/core/_ObjectBuilderSettingEvaluator.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
Expand Down Expand Up @@ -252,6 +253,11 @@ private Object fetchValue(boolean optional, boolean topLevel, boolean resolveVar
if (val != VOID) {
return val;
}

val = fetchMapLiteral(true);
if (val != VOID) {
return val;
}

val = fetchBuilderCall(true, topLevel);
if (val != VOID) {
Expand Down Expand Up @@ -512,6 +518,39 @@ private Object fetchListLiteral(boolean optional) throws _ObjectBuilderSettingEv
skipWS();
}
}

private Object fetchMapLiteral(boolean optional) throws _ObjectBuilderSettingEvaluationException {
if (pos == src.length() || src.charAt(pos) != '{') {
if (!optional) {
throw new _ObjectBuilderSettingEvaluationException("{", src, pos);
}
return VOID;
}
pos++;

MapExpression mapExp = new MapExpression();

while (true) {
skipWS();

if (fetchOptionalChar("}") != 0) {
return mapExp;
}
if (mapExp.itemCount() != 0) {
fetchRequiredChar(",");
skipWS();
}

Object key = fetchValue(false, false, true);
skipWS();
fetchRequiredChar(":");
skipWS();
Object value = fetchValue(false, false, true);
mapExp.addItem(new KeyValuePair(key, value));

skipWS();
}
}

private void skipWS() {
while (true) {
Expand Down Expand Up @@ -717,6 +756,43 @@ Object eval() throws _ObjectBuilderSettingEvaluationException {

}

private class MapExpression extends SettingExpression {

private List<KeyValuePair> items = new ArrayList();

void addItem(KeyValuePair item) {
items.add(item);
}

public int itemCount() {
return items.size();
}

@Override
Object eval() throws _ObjectBuilderSettingEvaluationException {
LinkedHashMap res = new LinkedHashMap(items.size() * 4 / 3, 1f);
for (KeyValuePair item : items) {
Object key = ensureEvaled(item.key);
if (key == null) {
throw new _ObjectBuilderSettingEvaluationException("Map can't use null as key.");
}
res.put(key, ensureEvaled(item.value));
}
return res;
}

}

private static class KeyValuePair {
private final Object key;
private final Object value;

public KeyValuePair(Object key, Object value) {
this.key = key;
this.value = value;
}
}

private class BuilderCallExpression extends ExpressionWithParameters {
private String className;

Expand Down
16 changes: 15 additions & 1 deletion src/manual/book.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25655,7 +25655,10 @@ TemplateModel x = env.getVariable("x"); // get variable x</programlisting>
<para>Fixes and improvements in the <quote>object
builder</quote> syntax used for configuring FreeMarker from
<literal>java.util.Properties</literal> (or other string-only
sources):</para>
sources). This is not to be confused with the template language
syntax, which has nothing to do with the <quote>object
builder</quote> syntax we are writing about here. The
improvements are:</para>

<itemizedlist>
<listitem>
Expand Down Expand Up @@ -25687,6 +25690,17 @@ TemplateModel x = env.getVariable("x"); // get variable x</programlisting>
<replaceable>itemN</replaceable> ]</literal> syntax.</para>
</listitem>

<listitem>
<para>Added support for creating maps with <literal>{
<replaceable>key1</replaceable>:
<replaceable>value1</replaceable>,
<replaceable>key2</replaceable>:
<replaceable>value2</replaceable>,
<replaceable>...</replaceable>
<replaceable>keyN</replaceable>:
<replaceable>valueN</replaceable> }</literal> syntax.</para>
</listitem>

<listitem>
<para>Number without decimal point will now be parsed to
<literal>Integer</literal>, <literal>Long</literal>, or
Expand Down
133 changes: 131 additions & 2 deletions src/test/java/freemarker/core/ObjectBuilderSettingsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,17 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TimeZone;

import org.junit.Test;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;

import freemarker.cache.CacheStorage;
import freemarker.cache.MruCacheStorage;
Expand All @@ -51,6 +55,7 @@
import freemarker.template.Version;
import freemarker.template.utility.WriteProtectable;

@SuppressWarnings("boxing")
public class ObjectBuilderSettingsTest {

@Test
Expand Down Expand Up @@ -667,7 +672,6 @@ public void testNumberLiteralJavaTypes() throws Exception {
Number.class, true, _SettingEvaluationEnvironment.getCurrent()));
}

@SuppressWarnings("boxing")
@Test
public void testListLiterals() throws Exception {
{
Expand Down Expand Up @@ -740,6 +744,117 @@ public void testListLiterals() throws Exception {
assertThat(e.getMessage(), containsString("end of"));
}
}

@Test
public void testMapLiterals() throws Exception {
{
HashMap<String, Object> expected = new HashMap();
expected.put("k1", "s");
expected.put("k2", null);
expected.put("k3", true);
expected.put("k4", new TestBean9(1));
expected.put("k5", ImmutableList.of(11, 22, 33));
assertEquals(expected, _ObjectBuilderSettingEvaluator.eval(
"{'k1': 's', 'k2': null, 'k3': true, "
+ "'k4': freemarker.core.ObjectBuilderSettingsTest$TestBean9(1), 'k5': [11, 22, 33]}",
Object.class, false, _SettingEvaluationEnvironment.getCurrent()));
assertEquals(expected, _ObjectBuilderSettingEvaluator.eval(
" { 'k1' : 's' , 'k2' : null , 'k3' : true , "
+ "'k4' : freemarker.core.ObjectBuilderSettingsTest$TestBean9 ( 1 ) , 'k5' : [ 11 , 22 , 33 ] } ",
Map.class, false, _SettingEvaluationEnvironment.getCurrent()));
assertEquals(expected, _ObjectBuilderSettingEvaluator.eval(
" {'k1':'s','k2':null,'k3':true,"
+ "'k4':freemarker.core.ObjectBuilderSettingsTest$TestBean9(1),'k5':[11,22,33]}",
LinkedHashMap.class, false, _SettingEvaluationEnvironment.getCurrent()));
}

{
HashMap<Object, String> expected = new HashMap();
expected.put(true, "T");
expected.put(1, "O");
expected.put(new TestBean9(1), "B");
expected.put(ImmutableList.of(11, 22, 33), "L");
assertEquals(expected, _ObjectBuilderSettingEvaluator.eval(
"{ true: 'T', 1: 'O', freemarker.core.ObjectBuilderSettingsTest$TestBean9(1): 'B', "
+ "[11, 22, 33]: 'L' }",
Object.class, false, _SettingEvaluationEnvironment.getCurrent()));
}

assertEquals(Collections.emptyMap(), _ObjectBuilderSettingEvaluator.eval(
"{}",
Object.class, false, _SettingEvaluationEnvironment.getCurrent()));
assertEquals(Collections.emptyMap(), _ObjectBuilderSettingEvaluator.eval(
"{ }",
Object.class, false, _SettingEvaluationEnvironment.getCurrent()));

assertEquals(Collections.singletonMap("k1", 123), _ObjectBuilderSettingEvaluator.eval(
"{'k1':123}",
Object.class, false, _SettingEvaluationEnvironment.getCurrent()));
assertEquals(Collections.singletonMap("k1", 123), _ObjectBuilderSettingEvaluator.eval(
"{ 'k1' : 123 }",
Object.class, false, _SettingEvaluationEnvironment.getCurrent()));

assertEquals(new TestBean9(1, ImmutableMap.of(11, "a", 22, "b")), _ObjectBuilderSettingEvaluator.eval(
"freemarker.core.ObjectBuilderSettingsTest$TestBean9(1, { 11: 'a', 22: 'b' })",
Object.class, false, _SettingEvaluationEnvironment.getCurrent()));

try {
_ObjectBuilderSettingEvaluator.eval(
"{1:2,}",
Object.class, false, _SettingEvaluationEnvironment.getCurrent());
fail();
} catch (_ObjectBuilderSettingEvaluationException e) {
assertThat(e.getMessage(), containsString("found character \"}\""));
}
try {
_ObjectBuilderSettingEvaluator.eval(
"{,1:2}",
Object.class, false, _SettingEvaluationEnvironment.getCurrent());
fail();
} catch (_ObjectBuilderSettingEvaluationException e) {
assertThat(e.getMessage(), containsString("found character \",\""));
}
try {
_ObjectBuilderSettingEvaluator.eval(
"1:2}",
Object.class, false, _SettingEvaluationEnvironment.getCurrent());
fail();
} catch (_ObjectBuilderSettingEvaluationException e) {
assertThat(e.getMessage(), containsString("found character \":\""));
}
try {
_ObjectBuilderSettingEvaluator.eval(
"1}",
Object.class, false, _SettingEvaluationEnvironment.getCurrent());
fail();
} catch (_ObjectBuilderSettingEvaluationException e) {
assertThat(e.getMessage(), containsString("found character \"}\""));
}
try {
_ObjectBuilderSettingEvaluator.eval(
"{1",
Object.class, false, _SettingEvaluationEnvironment.getCurrent());
fail();
} catch (_ObjectBuilderSettingEvaluationException e) {
assertThat(e.getMessage(), containsString("end of"));
}
try {
_ObjectBuilderSettingEvaluator.eval(
"{1:",
Object.class, false, _SettingEvaluationEnvironment.getCurrent());
fail();
} catch (_ObjectBuilderSettingEvaluationException e) {
assertThat(e.getMessage(), containsString("end of"));
}
try {
_ObjectBuilderSettingEvaluator.eval(
"{null:1}",
Object.class, false, _SettingEvaluationEnvironment.getCurrent());
fail();
} catch (_ObjectBuilderSettingEvaluationException e) {
assertThat(e.getMessage(), containsString("null as key"));
}
}

@Test
public void visibilityTest() throws Exception {
Expand Down Expand Up @@ -1124,21 +1239,32 @@ public static class TestBean9 {

private final int n;
private final List<?> list;
private final Map<?, ?> map;

public TestBean9(int n) {
this(n, null);
this(n, null, null);
}

public TestBean9(int n, List<?> list) {
this(n, list, null);
}

public TestBean9(int n, Map<?, ?> map) {
this(n, null, map);
}

public TestBean9(int n, List<?> list, Map<?, ?> map) {
this.n = n;
this.list = list;
this.map = map;
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((list == null) ? 0 : list.hashCode());
result = prime * result + ((map == null) ? 0 : map.hashCode());
result = prime * result + n;
return result;
}
Expand All @@ -1152,6 +1278,9 @@ public boolean equals(Object obj) {
if (list == null) {
if (other.list != null) return false;
} else if (!list.equals(other.list)) return false;
if (map == null) {
if (other.map != null) return false;
} else if (!map.equals(other.map)) return false;
if (n != other.n) return false;
return true;
}
Expand Down

0 comments on commit 1b429d8

Please sign in to comment.