You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2018/09/11 22:03:27 UTC
[2/2] freemarker git commit: Added
TemplateModelUtils.wrapAsHashUnion(ObjectWrapper,
List>) and wrapAsHashUnion(ObjectWrapper, Object...),
which meant to be used when you want to compose a data-model from multiple
objects in a way so that their entries
Added TemplateModelUtils.wrapAsHashUnion(ObjectWrapper, List<?>) and wrapAsHashUnion(ObjectWrapper, Object...), which meant to be used when you want to compose a data-model from multiple objects in a way so that their entries (Map key-value pairs, bean properties, etc.) appear together on the top level of the data-model.
Project: http://git-wip-us.apache.org/repos/asf/freemarker/repo
Commit: http://git-wip-us.apache.org/repos/asf/freemarker/commit/e8ef9937
Tree: http://git-wip-us.apache.org/repos/asf/freemarker/tree/e8ef9937
Diff: http://git-wip-us.apache.org/repos/asf/freemarker/diff/e8ef9937
Branch: refs/heads/2.3-gae
Commit: e8ef993776d4288b54e9f20ef0534542b6a77e17
Parents: 764007c
Author: ddekany <dd...@apache.org>
Authored: Wed Sep 12 00:03:04 2018 +0200
Committer: ddekany <dd...@apache.org>
Committed: Wed Sep 12 00:03:04 2018 +0200
----------------------------------------------------------------------
.../template/utility/TemplateModelUtils.java | 210 +++++++++++++++++++
src/manual/en_US/book.xml | 33 +++
.../template/utility/TemplateModelUtilTest.java | 146 ++++++++++++-
3 files changed, 387 insertions(+), 2 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/freemarker/blob/e8ef9937/src/main/java/freemarker/template/utility/TemplateModelUtils.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/template/utility/TemplateModelUtils.java b/src/main/java/freemarker/template/utility/TemplateModelUtils.java
index d33b6da..a277fb7 100644
--- a/src/main/java/freemarker/template/utility/TemplateModelUtils.java
+++ b/src/main/java/freemarker/template/utility/TemplateModelUtils.java
@@ -18,7 +18,20 @@
*/
package freemarker.template.utility;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import freemarker.core.CollectionAndSequence;
import freemarker.core._MessageUtil;
+import freemarker.template.Configuration;
+import freemarker.template.ObjectWrapper;
+import freemarker.template.SimpleSequence;
+import freemarker.template.TemplateCollectionModel;
+import freemarker.template.TemplateHashModel;
import freemarker.template.TemplateHashModelEx;
import freemarker.template.TemplateHashModelEx2;
import freemarker.template.TemplateHashModelEx2.KeyValuePair;
@@ -45,6 +58,8 @@ public final class TemplateModelUtils {
* {@link TemplateHashModelEx}-s, as with this you don't have to handle non-{@link TemplateHashModelEx2}-s
* separately. For non-{@link TemplateHashModelEx2} values the iteration will throw {@link TemplateModelException}
* if it reaches a key that's not a string ({@link TemplateScalarModel}).
+ *
+ * @since 2.3.28
*/
public static final TemplateHashModelEx2.KeyValuePairIterator getKeyValuePairIterator(TemplateHashModelEx hash)
throws TemplateModelException {
@@ -87,4 +102,199 @@ public final class TemplateModelUtils {
}
+ /**
+ * Same as {@link #wrapAsHashUnion(ObjectWrapper, List)}, but uses a varargs parameter instead of a {@link List}.
+ *
+ * @since 2.3.28
+ */
+ public static TemplateHashModel wrapAsHashUnion(ObjectWrapper objectWrapper, Object... hashLikeObjects)
+ throws TemplateModelException {
+ return wrapAsHashUnion(objectWrapper, Arrays.asList(hashLikeObjects));
+ }
+
+ /**
+ * Creates a {@link TemplateHashModel} that is the union of the hash-like objects passed in as argument. Hash-like
+ * here means that the argument {@link ObjectWrapper} will wrap it into an {@link TemplateModel} that implements
+ * {@link TemplateHashModel}, or it's already a {@link TemplateHashModel}. (Typical hash-like objects are JavaBeans
+ * and {@link Map}-s, though it depends on the {@link ObjectWrapper}.)
+ *
+ * <p>
+ * This method is typical used when you want to compose a data-model from multiple objects in a way so that their
+ * entries ({@link Map} key-value pairs, bean properties, etc.) appear together on the top level of the data-model.
+ * In such case, use the return value of this method as the combined data-model. Note that this functionality
+ * somewhat overlaps with {@link Configuration#setSharedVaribles(Map)}; check if that fits your use case better.
+ *
+ * @param objectWrapper
+ * {@link ObjectWrapper} used to wrap the elements of {@code hashLikeObjects}, except those that are
+ * already {@link TemplateModel}-s. Usually, you should pass in {@link Configuration#getObjectWrapper()}
+ * here.
+ * @param hashLikeObjects
+ * Hash-like objects whose union the result hash will be. The content of these hash-like objects must not
+ * change, or else the behavior of the resulting hash can be erratic. If multiple hash-like object
+ * contains the same key, then the value from the last such hash-like object wins. The oder of keys is
+ * kept, with the keys of earlier hash-like object object coming first (even if their values were
+ * replaced by a later hash-like object). This argument can't be {@code null}, but the list can contain
+ * {@code null} elements, which will be silently ignored. The list can be empty, in which case the result
+ * is an empty hash.
+ *
+ * @return The {@link TemplateHashModel} that's the union of the objects provided. This is a "view", that delegates
+ * to the underlying hashes, not a copy. If all elements are
+ *
+ * @throws TemplateModelException
+ * If wrapping an element of {@code hashLikeObjects} fails with {@link TemplateModelException}, or if
+ * wrapping an element results in a {@link TemplateModel} that's not a {@link TemplateHashModel}, or if
+ * the element was already a {@link TemplateModel} that isn't a {@link TemplateHashModel}.
+ *
+ * @since 2.3.28
+ */
+ @SuppressWarnings("unchecked")
+ public static TemplateHashModel wrapAsHashUnion(ObjectWrapper objectWrapper, List<?> hashLikeObjects)
+ throws TemplateModelException {
+ NullArgumentException.check("hashLikeObjects", hashLikeObjects);
+
+ List<TemplateHashModel> hashes = new ArrayList<TemplateHashModel>(hashLikeObjects.size());
+
+ boolean allTHMEx = true;
+ for (Object hashLikeObject : hashLikeObjects) {
+ if (hashLikeObject == null) {
+ continue;
+ }
+
+ TemplateModel tm;
+ if (hashLikeObject instanceof TemplateModel) {
+ tm = (TemplateModel) hashLikeObject;
+ } else {
+ tm = objectWrapper.wrap(hashLikeObject);
+ }
+
+ if (!(tm instanceof TemplateHashModelEx)) {
+ allTHMEx = false;
+ if (!(tm instanceof TemplateHashModel)) {
+ throw new TemplateModelException(
+ "One of the objects of the hash union is not hash-like: "
+ + ClassUtil.getFTLTypeDescription(tm));
+ }
+ }
+
+ hashes.add((TemplateHashModel) tm);
+ }
+
+ return hashes.isEmpty() ? Constants.EMPTY_HASH
+ : hashes.size() == 1 ? hashes.get(0)
+ : allTHMEx ? new HashExUnionModel((List<? extends TemplateHashModelEx>) hashes)
+ : new HashUnionModel(hashes);
+ }
+
+ private static class HashUnionModel implements TemplateHashModel {
+ private final List<? extends TemplateHashModel> hashes;
+
+ HashUnionModel(List<? extends TemplateHashModel> hashes) {
+ this.hashes = hashes;
+ }
+
+ public TemplateModel get(String key) throws TemplateModelException {
+ for (int i = hashes.size() - 1; i >= 0; i--) {
+ TemplateModel value = hashes.get(i).get(key);
+ if (value != null) {
+ return value;
+ }
+ }
+ return null;
+ }
+
+ public boolean isEmpty() throws TemplateModelException {
+ for (TemplateHashModel hash : hashes) {
+ if (!hash.isEmpty()) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ private static final class HashExUnionModel implements TemplateHashModelEx {
+ private final List<? extends TemplateHashModelEx> hashes;
+ private CollectionAndSequence keys;
+ private CollectionAndSequence values;
+ private int size;
+
+ private HashExUnionModel(List<? extends TemplateHashModelEx> hashes) {
+ this.hashes = hashes;
+ }
+
+ public TemplateModel get(String key) throws TemplateModelException {
+ for (int i = hashes.size() - 1; i >= 0; i--) {
+ TemplateModel value = hashes.get(i).get(key);
+ if (value != null) {
+ return value;
+ }
+ }
+ return null;
+ }
+
+ public boolean isEmpty() throws TemplateModelException {
+ for (TemplateHashModel hash : hashes) {
+ if (!hash.isEmpty()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public int size() throws TemplateModelException {
+ initKeys();
+ return size;
+ }
+
+ public TemplateCollectionModel keys()
+ throws TemplateModelException {
+ initKeys();
+ return keys;
+ }
+
+ public TemplateCollectionModel values() throws TemplateModelException {
+ initValues();
+ return values;
+ }
+
+ private void initKeys() throws TemplateModelException {
+ if (keys == null) {
+ Set keySet = new HashSet();
+ SimpleSequence keySeq = new SimpleSequence((ObjectWrapper) null);
+ for (TemplateHashModelEx hash : hashes) {
+ addKeys(keySet, keySeq, hash);
+ }
+ size = keySet.size();
+ keys = new CollectionAndSequence(keySeq);
+ }
+ }
+
+ private static void addKeys(Set keySet, SimpleSequence keySeq, TemplateHashModelEx hash)
+ throws TemplateModelException {
+ TemplateModelIterator it = hash.keys().iterator();
+ while (it.hasNext()) {
+ TemplateScalarModel tsm = (TemplateScalarModel) it.next();
+ if (keySet.add(tsm.getAsString())) {
+ // The first occurrence of the key decides the index;
+ // this is consistent with the behavior of java.util.LinkedHashSet.
+ keySeq.add(tsm);
+ }
+ }
+ }
+
+ private void initValues()
+ throws TemplateModelException {
+ if (values == null) {
+ SimpleSequence seq = new SimpleSequence(size(), null);
+ // Note: size() invokes initKeys() if needed.
+
+ int ln = keys.size();
+ for (int i = 0; i < ln; i++) {
+ seq.add(get(((TemplateScalarModel) keys.get(i)).getAsString()));
+ }
+ values = new CollectionAndSequence(seq);
+ }
+ }
+ }
+
}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/freemarker/blob/e8ef9937/src/manual/en_US/book.xml
----------------------------------------------------------------------
diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml
index 3f43b0b..bf08308 100644
--- a/src/manual/en_US/book.xml
+++ b/src/manual/en_US/book.xml
@@ -27613,6 +27613,39 @@ TemplateModel x = env.getVariable("x"); // get variable x</programlisting>
<appendix xml:id="app_versions">
<title>Version history</title>
+ <section xml:id="versions_2_3_29">
+ <title>2.3.29</title>
+
+ <para>Release date: [FIXME]</para>
+
+ <section>
+ <title>Changes on the FTL side</title>
+
+ <itemizedlist>
+ <listitem>
+ <para>[FIXME]</para>
+ </listitem>
+ </itemizedlist>
+ </section>
+
+ <section>
+ <title>Changes on the Java side</title>
+
+ <itemizedlist>
+ <listitem>
+ <para>Added
+ <literal>TemplateModelUtils.wrapAsHashUnion(ObjectWrapper,
+ List<?>)</literal> and
+ <literal>wrapAsHashUnion(ObjectWrapper, Object...)</literal>,
+ which meant to be used when you want to compose a data-model
+ from multiple objects in a way so that their entries
+ (<literal>Map</literal> key-value pairs, bean properties, etc.)
+ appear together on the top level of the data-model.</para>
+ </listitem>
+ </itemizedlist>
+ </section>
+ </section>
+
<section xml:id="versions_2_3_28">
<title>2.3.28</title>
http://git-wip-us.apache.org/repos/asf/freemarker/blob/e8ef9937/src/test/java/freemarker/template/utility/TemplateModelUtilTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/template/utility/TemplateModelUtilTest.java b/src/test/java/freemarker/template/utility/TemplateModelUtilTest.java
index c93a2e5..47d7bc2 100644
--- a/src/test/java/freemarker/template/utility/TemplateModelUtilTest.java
+++ b/src/test/java/freemarker/template/utility/TemplateModelUtilTest.java
@@ -22,27 +22,39 @@ package freemarker.template.utility;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
+import java.util.Map.Entry;
import org.junit.Test;
+import com.google.common.collect.ImmutableMap;
+
import freemarker.template.Configuration;
import freemarker.template.DefaultMapAdapter;
import freemarker.template.DefaultNonListCollectionAdapter;
+import freemarker.template.DefaultObjectWrapper;
import freemarker.template.DefaultObjectWrapperBuilder;
+import freemarker.template.ObjectWrapper;
import freemarker.template.TemplateCollectionModel;
+import freemarker.template.TemplateHashModel;
import freemarker.template.TemplateHashModelEx;
import freemarker.template.TemplateHashModelEx2;
import freemarker.template.TemplateHashModelEx2.KeyValuePair;
import freemarker.template.TemplateHashModelEx2.KeyValuePairIterator;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateModelIterator;
import freemarker.template.TemplateNumberModel;
import freemarker.template.TemplateScalarModel;
public class TemplateModelUtilTest {
+ private final DefaultObjectWrapper ow = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_28).build();
+
@Test
public void testGetKeyValuePairIterator() throws Exception {
Map<Object, Object> map = new LinkedHashMap<Object, Object>();
@@ -82,8 +94,7 @@ public class TemplateModelUtilTest {
@Test
public void testGetKeyValuePairIteratorWithEx2() throws Exception {
Map<Object, Object> map = new LinkedHashMap<Object, Object>();
- TemplateHashModelEx thme = DefaultMapAdapter.adapt(
- map, new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_27).build());
+ TemplateHashModelEx thme = DefaultMapAdapter.adapt(map, ow);
assertetGetKeyValuePairIteratorContent("", thme);
@@ -122,6 +133,111 @@ public class TemplateModelUtilTest {
throw new IllegalArgumentException("Type unsupported by test: " + model.getClass().getName());
}
+ @Test
+ public void wrapAsHashUnion1() throws TemplateModelException {
+ TemplateHashModelEx thEx1 = new TemplateHashModelExOnly(ImmutableMap.of("a", 1, "b", 2));
+ TemplateHashModelEx thEx2 = new TemplateHashModelExOnly(ImmutableMap.of("c", 3, "d", 4));
+ TemplateHashModelEx thEx3 = new TemplateHashModelExOnly(ImmutableMap.of("b", 22, "c", 33));
+ TemplateHashModelEx thEx4 = new TemplateHashModelExOnly(Collections.emptyMap());
+ TemplateHashModel th1 = new TemplateHashModelOnly(ImmutableMap.of("a", 1, "b", 2));
+ TemplateHashModel th2 = new TemplateHashModelOnly(ImmutableMap.of("c", 3, "d", 4));
+ TemplateHashModel th3 = new TemplateHashModelOnly(ImmutableMap.of("b", 22, "c", 33));
+ TemplateHashModel th4 = new TemplateHashModelOnly(Collections.emptyMap());
+
+ assertUnionResult(ImmutableMap.of("a", 1, "b", 2, "c", 3, "d", 4), true,
+ TemplateModelUtils.wrapAsHashUnion(ow, thEx1, thEx2));
+ assertUnionResult(ImmutableMap.of("a", 1, "b", 2, "c", 3, "d", 4), false,
+ TemplateModelUtils.wrapAsHashUnion(ow, th1, th2));
+ assertUnionResult(ImmutableMap.of("a", 1, "b", 2, "c", 3, "d", 4), false,
+ TemplateModelUtils.wrapAsHashUnion(ow, thEx1, th2));
+ assertUnionResult(ImmutableMap.of("a", 1, "b", 2, "c", 3, "d", 4), false,
+ TemplateModelUtils.wrapAsHashUnion(ow, th1, thEx2));
+
+ assertUnionResult(ImmutableMap.of("a", 1, "b", 22, "c", 33), true,
+ TemplateModelUtils.wrapAsHashUnion(ow, thEx1, thEx3));
+ assertUnionResult(ImmutableMap.of("a", 1, "b", 22, "c", 33), false,
+ TemplateModelUtils.wrapAsHashUnion(ow, th1, th3));
+ assertUnionResult(ImmutableMap.of("b", 2, "c", 33, "a", 1), true,
+ TemplateModelUtils.wrapAsHashUnion(ow, thEx3, thEx1));
+ assertUnionResult(ImmutableMap.of("b", 2, "c", 33, "a", 1), false,
+ TemplateModelUtils.wrapAsHashUnion(ow, th3, th1));
+
+ assertUnionResult(ImmutableMap.of("a", 1, "b", 2), true,
+ TemplateModelUtils.wrapAsHashUnion(ow, thEx1, thEx4));
+ assertUnionResult(ImmutableMap.of("a", 1, "b", 2), true,
+ TemplateModelUtils.wrapAsHashUnion(ow, thEx4, thEx1));
+ assertUnionResult(ImmutableMap.of("a", 1, "b", 2), false,
+ TemplateModelUtils.wrapAsHashUnion(ow, th1, th4));
+ assertUnionResult(ImmutableMap.of("a", 1, "b", 2), false,
+ TemplateModelUtils.wrapAsHashUnion(ow, th4, th1));
+ assertUnionResult(Collections.<String, Integer>emptyMap(), true,
+ TemplateModelUtils.wrapAsHashUnion(ow, thEx4, thEx4));
+ assertUnionResult(Collections.<String, Integer>emptyMap(), false,
+ TemplateModelUtils.wrapAsHashUnion(ow, th4, th4));
+ }
+
+ @Test
+ public void wrapAsHashUnionWrapping() throws TemplateModelException {
+ TemplateHashModel h = TemplateModelUtils.wrapAsHashUnion(ow,
+ ImmutableMap.of("a", 1), new MyBean(), null, ow.wrap(ImmutableMap.of("c", 3)));
+ assertThat(h, instanceOf(TemplateHashModelEx.class));
+ assertEquals(((TemplateNumberModel) h.get("a")).getAsNumber(), 1);
+ assertEquals(((TemplateNumberModel) h.get("b")).getAsNumber(), 2);
+ assertEquals(((TemplateNumberModel) h.get("c")).getAsNumber(), 3);
+ assertNotNull(h.get("class"));
+
+ try {
+ TemplateModelUtils.wrapAsHashUnion(ow, "x");
+ fail();
+ } catch (TemplateModelException e) {
+ // Expected
+ }
+ }
+
+ @Test
+ public void wrapAsHashUnionSizeEdgeCases() throws TemplateModelException {
+ assertSame(Constants.EMPTY_HASH, TemplateModelUtils.wrapAsHashUnion(ow));
+ assertSame(Constants.EMPTY_HASH, TemplateModelUtils.wrapAsHashUnion(ow, null, null));
+
+ TemplateModel hash = ow.wrap(ImmutableMap.of("a", 1));
+ assertSame(hash, TemplateModelUtils.wrapAsHashUnion(ow, hash));
+ assertSame(hash, TemplateModelUtils.wrapAsHashUnion(ow, null, hash, null));
+ }
+
+ private void assertUnionResult(
+ Map<String, Integer> expected, boolean expectHashEx,
+ TemplateHashModel actual) throws TemplateModelException {
+ assertTrue(expectHashEx == actual instanceof TemplateHashModelEx);
+
+ for (Entry<String, Integer> kvp : expected.entrySet()) {
+ TemplateModel tmValue = actual.get(kvp.getKey());
+ assertNotNull(tmValue);
+ assertEquals(kvp.getValue(), ((TemplateNumberModel) tmValue).getAsNumber());
+ }
+
+ assertEquals(expected.isEmpty(), actual.isEmpty());
+
+ if (actual instanceof TemplateHashModelEx) {
+ TemplateHashModelEx actualEx = (TemplateHashModelEx) actual;
+
+ assertEquals(expected.size(), actualEx.size());
+
+ List<String> expectedKeys = new ArrayList<String>(expected.keySet());
+ List<String> actualKeys = new ArrayList<String>();
+ for (TemplateModelIterator it = ((TemplateHashModelEx) actual).keys().iterator(); it.hasNext(); ) {
+ actualKeys.add(((TemplateScalarModel) it.next()).getAsString());
+ }
+ assertEquals(expectedKeys, actualKeys);
+
+ List<Integer> expectedValues = new ArrayList<Integer>(expected.values());
+ List<Integer> actualValues = new ArrayList<Integer>();
+ for (TemplateModelIterator it = ((TemplateHashModelEx) actual).values().iterator(); it.hasNext(); ) {
+ actualValues.add((Integer) ((TemplateNumberModel) it.next()).getAsNumber());
+ }
+ assertEquals(expectedValues, actualValues);
+ }
+ }
+
/**
* Deliberately doesn't implement {@link TemplateHashModelEx2}, only {@link TemplateHashModelEx}.
*/
@@ -157,4 +273,30 @@ public class TemplateModelUtilTest {
}
+ private static class TemplateHashModelOnly implements TemplateHashModel {
+
+ private final Map<?, ?> map;
+ private final ObjectWrapper objectWrapper;
+
+ public TemplateHashModelOnly(Map<?, ?> map) {
+ this.map = map;
+ objectWrapper = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_27).build();
+ }
+
+ public TemplateModel get(String key) throws TemplateModelException {
+ return objectWrapper.wrap(map.get(key));
+ }
+
+ public boolean isEmpty() throws TemplateModelException {
+ return map.isEmpty();
+ }
+
+ }
+
+ public static class MyBean {
+ public int getB() {
+ return 2;
+ }
+ }
+
}