Repository: incubator-freemarker Updated Branches: refs/heads/2.3-gae a67111065 -> 2d0b49319
New built-in, sequence. This can be used to work around situations where a listable value lacks some features that you need in the template (like it can't be listed twice, it can't tell its size, etc.), and you can't modify the data-model to fix the problem. See more... Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/2d0b4931 Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/2d0b4931 Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/2d0b4931 Branch: refs/heads/2.3-gae Commit: 2d0b49319a216e610a5aa0ade5dd24ae7881457d Parents: a671110 Author: ddekany <[email protected]> Authored: Sun Sep 17 18:28:30 2017 +0200 Committer: ddekany <[email protected]> Committed: Sun Sep 17 18:28:30 2017 +0200 ---------------------------------------------------------------------- src/main/java/freemarker/core/BuiltIn.java | 4 +- .../freemarker/core/BuiltInsForSequences.java | 29 +++++++ .../freemarker/core/CollectionAndSequence.java | 11 ++- .../freemarker/core/NonSequenceException.java | 9 ++- .../core/UnexpectedTypeException.java | 12 +++ src/manual/en_US/book.xml | 84 ++++++++++++++++++-- .../freemarker/core/SequenceBuiltInTest.java | 82 +++++++++++++++++++ 7 files changed, 216 insertions(+), 15 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/2d0b4931/src/main/java/freemarker/core/BuiltIn.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/core/BuiltIn.java b/src/main/java/freemarker/core/BuiltIn.java index b4e3239..630f895 100644 --- a/src/main/java/freemarker/core/BuiltIn.java +++ b/src/main/java/freemarker/core/BuiltIn.java @@ -61,6 +61,7 @@ import freemarker.core.BuiltInsForSequences.lastBI; import freemarker.core.BuiltInsForSequences.reverseBI; import freemarker.core.BuiltInsForSequences.seq_containsBI; import freemarker.core.BuiltInsForSequences.seq_index_ofBI; +import freemarker.core.BuiltInsForSequences.sequenceBI; import freemarker.core.BuiltInsForSequences.sortBI; import freemarker.core.BuiltInsForSequences.sort_byBI; import freemarker.core.BuiltInsForStringsMisc.evalBI; @@ -83,7 +84,7 @@ abstract class BuiltIn extends Expression implements Cloneable { static final Set<String> CAMEL_CASE_NAMES = new TreeSet<String>(); static final Set<String> SNAKE_CASE_NAMES = new TreeSet<String>(); - static final int NUMBER_OF_BIS = 263; + static final int NUMBER_OF_BIS = 264; static final HashMap<String, BuiltIn> BUILT_INS_BY_NAME = new HashMap(NUMBER_OF_BIS * 3 / 2 + 1, 1f); static { @@ -262,6 +263,7 @@ abstract class BuiltIn extends Expression implements Cloneable { putBI("seq_contains", "seqContains", new seq_containsBI()); putBI("seq_index_of", "seqIndexOf", new seq_index_ofBI(true)); putBI("seq_last_index_of", "seqLastIndexOf", new seq_index_ofBI(false)); + putBI("sequence", new sequenceBI()); putBI("short", new shortBI()); putBI("size", new BuiltInsForMultipleTypes.sizeBI()); putBI("sort_by", "sortBy", new sort_byBI()); http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/2d0b4931/src/main/java/freemarker/core/BuiltInsForSequences.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/core/BuiltInsForSequences.java b/src/main/java/freemarker/core/BuiltInsForSequences.java index 8430133..f2d1c6a 100644 --- a/src/main/java/freemarker/core/BuiltInsForSequences.java +++ b/src/main/java/freemarker/core/BuiltInsForSequences.java @@ -30,8 +30,10 @@ import java.util.List; import freemarker.ext.beans.CollectionModel; import freemarker.template.SimpleNumber; import freemarker.template.SimpleScalar; +import freemarker.template.SimpleSequence; import freemarker.template.TemplateBooleanModel; import freemarker.template.TemplateCollectionModel; +import freemarker.template.TemplateCollectionModelEx; import freemarker.template.TemplateDateModel; import freemarker.template.TemplateException; import freemarker.template.TemplateHashModel; @@ -829,6 +831,33 @@ class BuiltInsForSequences { } + static class sequenceBI extends BuiltIn { + + @Override + TemplateModel _eval(Environment env) throws TemplateException { + TemplateModel model = target.eval(env); + + if (model instanceof TemplateSequenceModel && !isBuggySeqButGoodCollection(model)) { + return model; + } + + if (!(model instanceof TemplateCollectionModel)) { + throw new NonSequenceOrCollectionException(target, model, env); + } + TemplateCollectionModel coll = (TemplateCollectionModel) model; + + SimpleSequence seq = + coll instanceof TemplateCollectionModelEx + ? new SimpleSequence(((TemplateCollectionModelEx) coll).size()) + : new SimpleSequence(); + for (TemplateModelIterator iter = coll.iterator(); iter.hasNext(); ) { + seq.add(iter.next()); + } + return seq; + } + + } + private static boolean isBuggySeqButGoodCollection( TemplateModel model) { return model instanceof CollectionModel http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/2d0b4931/src/main/java/freemarker/core/CollectionAndSequence.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/core/CollectionAndSequence.java b/src/main/java/freemarker/core/CollectionAndSequence.java index 79e7576..33556ff 100644 --- a/src/main/java/freemarker/core/CollectionAndSequence.java +++ b/src/main/java/freemarker/core/CollectionAndSequence.java @@ -23,6 +23,7 @@ import java.io.Serializable; import java.util.ArrayList; import freemarker.template.TemplateCollectionModel; +import freemarker.template.TemplateCollectionModelEx; import freemarker.template.TemplateModel; import freemarker.template.TemplateModelException; import freemarker.template.TemplateModelIterator; @@ -30,13 +31,13 @@ import freemarker.template.TemplateSequenceModel; /** * Add sequence capabilities to an existing collection, or - * vice versa. Used by ?keys and ?values built-ins. + * vice versa. Used by the ?keys and ?values built-ins. */ final public class CollectionAndSequence implements TemplateCollectionModel, TemplateSequenceModel, Serializable { private TemplateCollectionModel collection; private TemplateSequenceModel sequence; - private ArrayList data; + private ArrayList<TemplateModel> data; public CollectionAndSequence(TemplateCollectionModel collection) { this.collection = collection; @@ -59,13 +60,15 @@ implements TemplateCollectionModel, TemplateSequenceModel, Serializable { return sequence.get(i); } else { initSequence(); - return (TemplateModel) data.get(i); + return data.get(i); } } public int size() throws TemplateModelException { if (sequence != null) { return sequence.size(); + } if (collection instanceof TemplateCollectionModelEx) { + return ((TemplateCollectionModelEx) collection).size(); } else { initSequence(); return data.size(); @@ -74,7 +77,7 @@ implements TemplateCollectionModel, TemplateSequenceModel, Serializable { private void initSequence() throws TemplateModelException { if (data == null) { - data = new ArrayList(); + data = new ArrayList<TemplateModel>(); TemplateModelIterator it = collection.iterator(); while (it.hasNext()) { data.add(it.next()); http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/2d0b4931/src/main/java/freemarker/core/NonSequenceException.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/core/NonSequenceException.java b/src/main/java/freemarker/core/NonSequenceException.java index 2b85ab9..21440bc 100644 --- a/src/main/java/freemarker/core/NonSequenceException.java +++ b/src/main/java/freemarker/core/NonSequenceException.java @@ -21,6 +21,7 @@ package freemarker.core; import freemarker.template.TemplateModel; import freemarker.template.TemplateSequenceModel; +import freemarker.template.utility.CollectionUtils; /** * Indicates that a {@link TemplateSequenceModel} value was expected, but the value had a different type. @@ -46,19 +47,19 @@ public class NonSequenceException extends UnexpectedTypeException { NonSequenceException( Expression blamed, TemplateModel model, Environment env) throws InvalidReferenceException { - super(blamed, model, "sequence", EXPECTED_TYPES, env); + this(blamed, model, CollectionUtils.EMPTY_OBJECT_ARRAY, env); } NonSequenceException( Expression blamed, TemplateModel model, String tip, Environment env) throws InvalidReferenceException { - super(blamed, model, "sequence", EXPECTED_TYPES, tip, env); + this(blamed, model, new Object[] { tip }, env); } NonSequenceException( - Expression blamed, TemplateModel model, String[] tips, Environment env) throws InvalidReferenceException { + Expression blamed, TemplateModel model, Object[] tips, Environment env) throws InvalidReferenceException { super(blamed, model, "sequence", EXPECTED_TYPES, tips, env); } - + } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/2d0b4931/src/main/java/freemarker/core/UnexpectedTypeException.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/core/UnexpectedTypeException.java b/src/main/java/freemarker/core/UnexpectedTypeException.java index 0878f81..61b0fe4 100644 --- a/src/main/java/freemarker/core/UnexpectedTypeException.java +++ b/src/main/java/freemarker/core/UnexpectedTypeException.java @@ -19,8 +19,13 @@ package freemarker.core; +import java.util.Arrays; + +import freemarker.template.TemplateCollectionModel; +import freemarker.template.TemplateCollectionModelEx; import freemarker.template.TemplateException; import freemarker.template.TemplateModel; +import freemarker.template.TemplateSequenceModel; /** * The type of a value differs from what was expected. @@ -88,6 +93,13 @@ public class UnexpectedTypeException extends TemplateException { errorDescBuilder.tip(tip); } } + if (model instanceof TemplateCollectionModel + && (Arrays.asList(expectedTypes).contains(TemplateSequenceModel.class) + || Arrays.asList(expectedTypes).contains(TemplateCollectionModelEx.class))) { + errorDescBuilder.tip("As the problematic value contains a collection of items, you could convert it " + + "to a sequence like someValue?sequence. Be sure though that you won't have a large number of " + + "items, as all will be held in memory one the same time."); + } return errorDescBuilder; } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/2d0b4931/src/manual/en_US/book.xml ---------------------------------------------------------------------- diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml index 21c3479..569cbac 100644 --- a/src/manual/en_US/book.xml +++ b/src/manual/en_US/book.xml @@ -12812,6 +12812,10 @@ grant codeBase "file:/path/to/freemarker.jar" </listitem> <listitem> + <para><link linkend="ref_builtin_sequence">sequence</link></para> + </listitem> + + <listitem> <para><link linkend="ref_builtin_sort_by">sort_by</link></para> </listitem> @@ -18439,6 +18443,61 @@ ${1305575275540?number_to_time}</programlisting> May 16, 2011 3:47:55 PM</programlisting> </section> + + <section xml:id="ref_builtin_sequence"> + <title>sequence</title> + + <indexterm> + <primary>seq_sequence built-in</primary> + </indexterm> + + <para>This built-in is used to convert a listable value (one that + you can iterate through with the <link + linkend="ref.directive.list"><literal>list</literal> + directive</link>) to a more capable <link + linkend="dgui_datamodel_container">sequence</link> value. Sequences + support operations like <literal>xs[index]</literal> and + <literal>xs?size</literal>. Also, the resulting value is listable + for multiple times, even if the original value was backed by a + <literal>java.util.Iterator</literal>. This built-in is typically + used to work around data-model problems, in case you can't fix the + data-model itself. If you can, always fix the data-model instead + (give a <literal>java.util.List</literal> or array to the template + instead of a more restricted object, like a + non-<literal>List</literal> <literal>java.util.Collection</literal>, + or a <literal>java.util.Iterator</literal>).</para> + + <para>If the value is already a sequence, then this built-in just + returns that as is. If the value is not something that the <link + linkend="ref.directive.list"><literal>list</literal> + directive</link> could list, then template processing will be + aborted with error. Otherwise, it fetches all the values, and stores + them into a sequence. Be careful if you can have a huge number of + items, as all of them will be held in memory on the same + time.</para> + + <para>You should convert a value with <literal>sequence</literal> + only once. If you need the resulting sequence at multiple places, + always assign the result to a variable, because if the value you + convert is only listable once, converting it for the second time + will result in error or an empty sequence. Also the conversion is + somewhat costly for big collections, so it's better to do it only + once.</para> + + <para>Example: Let's say you find that <literal>users</literal> is + only listable once (because it's a + <literal>java.util.Iterator</literal>), but you need to list it for + multiple times in the template, and you can't fix the data-model. + Then you could do this:</para> + + <programlisting role="template"><#-- Collect all the users into a sequence: --> +<#assign usersSeq = users?sequence> + +<#list usersSeq as user>...</#list> +Again: +<#list usersSeq as user>...</#list> +</programlisting> + </section> </section> </chapter> @@ -18494,6 +18553,11 @@ May 16, 2011 </listitem> <listitem> + <para><link + linkend="ref.directive.list.continue">continue</link></para> + </listitem> + + <listitem> <para><link linkend="ref.directive.default">default</link></para> </listitem> @@ -26989,11 +27053,10 @@ TemplateModel x = env.getVariable("x"); // get variable x</programlisting> <itemizedlist> <listitem> - <para>Added the <link - linkend="ref.directive.list.continue"><literal>continue</literal> - directive</link>, which can be used inside a - <literal>list</literal> to skip to the next iteration (similarly - as in Java).</para> + <para>New directive: <literal>continue</literal>. This can be + used inside the <literal>list</literal> directive to skip to the + next iteration (similarly as in Java). <link + linkend="ref.directive.list.continue">See more...</link></para> </listitem> <listitem> @@ -27010,6 +27073,15 @@ TemplateModel x = env.getVariable("x"); // get variable x</programlisting> </listitem> <listitem> + <para>New built-in, <literal>sequence</literal>. This can be + used to work around situations where a listable value lacks some + features that you need in the template (like it can't be listed + twice, it can't tell its size, etc.), and you can't modify the + data-model to fix the problem. <link + linkend="ref_builtin_sequence">See more...</link></para> + </listitem> + + <listitem> <para>Bug fixed (<link xlink:href="https://issues.apache.org/jira/browse/FREEMARKER-70">FREEMARKER-70</link>): The usage of loop variable built-ins, like @@ -27033,7 +27105,7 @@ TemplateModel x = env.getVariable("x"); // get variable x</programlisting> xlink:href="https://issues.apache.org/jira/browse/FREEMARKER-71">FREEMARKER-71</link>): When using <literal><replaceable>exp</replaceable>?eval</literal>, if the - expression inside evaluated string throws an exception, the + expression inside the evaluated string throws an exception, the cause exception of that exception was lost.</para> </listitem> </itemizedlist> http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/2d0b4931/src/test/java/freemarker/core/SequenceBuiltInTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/freemarker/core/SequenceBuiltInTest.java b/src/test/java/freemarker/core/SequenceBuiltInTest.java new file mode 100644 index 0000000..77eecc5 --- /dev/null +++ b/src/test/java/freemarker/core/SequenceBuiltInTest.java @@ -0,0 +1,82 @@ +package freemarker.core; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.io.IOException; + +import org.junit.Test; + +import com.google.common.collect.ImmutableSet; + +import freemarker.template.Configuration; +import freemarker.template.DefaultIterableAdapter; +import freemarker.template.DefaultNonListCollectionAdapter; +import freemarker.template.TemplateCollectionModelEx; +import freemarker.template.TemplateException; +import freemarker.template.TemplateModel; +import freemarker.template.TemplateSequenceModel; +import freemarker.template.utility.ObjectWrapperWithAPISupport; +import freemarker.test.TemplateTest; + +public class SequenceBuiltInTest extends TemplateTest { + + @Test + public void testWithCollection() throws TemplateException, IOException { + ObjectWrapperWithAPISupport ow = (ObjectWrapperWithAPISupport) getConfiguration().getObjectWrapper(); + + TemplateModel xs = DefaultIterableAdapter.adapt(ImmutableSet.of("a", "b"), ow); + assertThat(xs, not(instanceOf(TemplateCollectionModelEx.class))); + assertThat(xs, not(instanceOf(TemplateSequenceModel.class))); + addToDataModel("xs", xs); + + try { + assertOutput("${xs[1]}", "b"); + fail(); + } catch (TemplateException e) { + System.out.println(e); //!!T + assertThat(e.getMessage(), containsString("?sequence")); // Contains tip to use ?sequence + } + assertOutput("${xs?sequence[1]}", "b"); + + try { + assertOutput("${xs?size}", "2"); + fail(); + } catch (TemplateException e) { + System.out.println(e); //!!T + assertThat(e.getMessage(), containsString("?sequence")); // Contains tip to use ?sequence + } + assertOutput("${xs?sequence?size}", "2"); + } + + @Test + public void testWithCollectionEx() throws TemplateException, IOException { + ObjectWrapperWithAPISupport ow = (ObjectWrapperWithAPISupport) getConfiguration().getObjectWrapper(); + + TemplateModel xs = DefaultNonListCollectionAdapter.adapt(ImmutableSet.of("a", "b"), ow); + assertThat(xs, not(instanceOf(TemplateSequenceModel.class))); + assertThat(xs, instanceOf(TemplateCollectionModelEx.class)); + addToDataModel("xs", xs); + + try { + assertOutput("${xs[1]}", "b"); + fail(); + } catch (TemplateException e) { + assertThat(e.getMessage(), containsString("?sequence")); // Contains tip to use ?sequence + } + assertOutput("${xs?sequence[1]}", "b"); + + assertOutput("${xs?size}", "2"); // No need for ?sequence + } + + @Test + public void testWithSequence() throws TemplateException, IOException { + assertOutput("${[11, 12]?sequence[1]}", "12"); + + + getConfiguration().setIncompatibleImprovements(Configuration.VERSION_2_3_23); + // As it returns the sequence as is, it works with an infinite sequence: + assertOutput("${(11..)?sequence[1]}", "12"); + } + +}
