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&lt;?&gt;)</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;
+        }
+    }
+    
 }