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 2019/08/01 19:56:34 UTC

[freemarker] 02/03: Added ?take_while(predicate) and ?drop_while(predicate).

This is an automated email from the ASF dual-hosted git repository.

ddekany pushed a commit to branch 2.3-gae
in repository https://gitbox.apache.org/repos/asf/freemarker.git

commit 1053eef00b76182beebc62e1a9f289f8e2be1326
Author: ddekany <dd...@apache.org>
AuthorDate: Thu Aug 1 21:28:45 2019 +0200

    Added ?take_while(predicate) and ?drop_while(predicate).
---
 src/main/java/freemarker/core/BuiltIn.java         |   4 +-
 .../java/freemarker/core/BuiltInsForSequences.java | 195 +++++++++++++++++++--
 src/main/java/freemarker/core/_MessageUtil.java    |   6 +
 src/manual/en_US/book.xml                          |  12 +-
 .../java/freemarker/core/NullTransparencyTest.java |   3 +-
 .../core/TakeWhileAndDropWhileBiTest.java          | 148 ++++++++++++++++
 6 files changed, 345 insertions(+), 23 deletions(-)

diff --git a/src/main/java/freemarker/core/BuiltIn.java b/src/main/java/freemarker/core/BuiltIn.java
index 5322eed..5e143b1 100644
--- a/src/main/java/freemarker/core/BuiltIn.java
+++ b/src/main/java/freemarker/core/BuiltIn.java
@@ -84,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 = 281;
+    static final int NUMBER_OF_BIS = 285;
     static final HashMap<String, BuiltIn> BUILT_INS_BY_NAME = new HashMap(NUMBER_OF_BIS * 3 / 2 + 1, 1f);
 
     static {
@@ -109,6 +109,7 @@ abstract class BuiltIn extends Expression implements Cloneable {
         putBI("datetime_if_unknown", "datetimeIfUnknown", new BuiltInsForDates.dateType_if_unknownBI(TemplateDateModel.DATETIME));
         putBI("default", new BuiltInsForExistenceHandling.defaultBI());
         putBI("double", new doubleBI());
+        putBI("drop_while", "dropWhile", new BuiltInsForSequences.drop_whileBI());
         putBI("ends_with", "endsWith", new BuiltInsForStringsBasic.ends_withBI());
         putBI("ensure_ends_with", "ensureEndsWith", new BuiltInsForStringsBasic.ensure_ends_withBI());
         putBI("ensure_starts_with", "ensureStartsWith", new BuiltInsForStringsBasic.ensure_starts_withBI());
@@ -278,6 +279,7 @@ abstract class BuiltIn extends Expression implements Cloneable {
         putBI("starts_with", "startsWith", new BuiltInsForStringsBasic.starts_withBI());
         putBI("string", new BuiltInsForMultipleTypes.stringBI());
         putBI("substring", new BuiltInsForStringsBasic.substringBI());
+        putBI("take_while", "takeWhile", new BuiltInsForSequences.take_whileBI());
         putBI("then", new BuiltInsWithLazyConditionals.then_BI());
         putBI("time", new BuiltInsForMultipleTypes.dateBI(TemplateDateModel.TIME));
         putBI("time_if_unknown", "timeIfUnknown", new BuiltInsForDates.dateType_if_unknownBI(TemplateDateModel.TIME));
diff --git a/src/main/java/freemarker/core/BuiltInsForSequences.java b/src/main/java/freemarker/core/BuiltInsForSequences.java
index 7c8afc5..153be79 100644
--- a/src/main/java/freemarker/core/BuiltInsForSequences.java
+++ b/src/main/java/freemarker/core/BuiltInsForSequences.java
@@ -1000,7 +1000,27 @@ class BuiltInsForSequences {
         
     }
 
-    static class filterBI extends IntermediateStreamOperationLikeBuiltIn {
+    private static abstract class FilterLikeBI extends IntermediateStreamOperationLikeBuiltIn {
+        protected final boolean elementMatches(TemplateModel element, ElementTransformer elementTransformer,
+                Environment env)
+                throws TemplateException {
+            TemplateModel transformedElement = elementTransformer.transformElement(element, env);
+            if (!(transformedElement instanceof TemplateBooleanModel)) {
+                if (transformedElement == null) {
+                    throw new _TemplateModelException(getElementTransformerExp(), env,
+                            "The filter expression has returned no value (has returned null), " +
+                                    "rather than a boolean.");
+                }
+                throw new _TemplateModelException(getElementTransformerExp(), env,
+                        "The filter expression had to return a boolean value, but it returned ",
+                        new _DelayedAOrAn(new _DelayedFTLTypeDescription(transformedElement)),
+                        " instead.");
+            }
+            return ((TemplateBooleanModel) transformedElement).getAsBoolean();
+        }
+    }
+
+    static class filterBI extends FilterLikeBI {
 
         protected TemplateModel calculateResult(
                 final TemplateModelIterator lhoIterator, final TemplateModel lho,
@@ -1073,21 +1093,79 @@ class BuiltInsForSequences {
             }
         }
 
-        private boolean elementMatches(TemplateModel element, ElementTransformer elementTransformer, Environment env)
-                throws TemplateException {
-            TemplateModel transformedElement = elementTransformer.transformElement(element, env);
-            if (!(transformedElement instanceof TemplateBooleanModel)) {
-                if (transformedElement == null) {
-                    throw new _TemplateModelException(getElementTransformerExp(), env,
-                            "The filter expression has returned no value (has returned null), " +
-                            "rather than a boolean.");
+    }
+
+    static class take_whileBI extends FilterLikeBI {
+
+        protected TemplateModel calculateResult(
+                final TemplateModelIterator lhoIterator, final TemplateModel lho,
+                boolean lhoIsSequence, final ElementTransformer elementTransformer,
+                final Environment env) throws TemplateException {
+            if (!isLazilyGeneratedResultEnabled()) {
+                if (!lhoIsSequence) {
+                    throw _MessageUtil.newLazilyGeneratedCollectionMustBeSequenceException(take_whileBI.this);
                 }
-                throw new _TemplateModelException(getElementTransformerExp(), env,
-                        "The filter expression had to return a boolean value, but it returned ",
-                        new _DelayedAOrAn(new _DelayedFTLTypeDescription(transformedElement)),
-                        " instead.");
+
+                List<TemplateModel> resultList = new ArrayList<TemplateModel>();
+                while (lhoIterator.hasNext()) {
+                    TemplateModel element = lhoIterator.next();
+                    if (elementMatches(element, elementTransformer, env)) {
+                        resultList.add(element);
+                    } else {
+                        break;
+                    }
+                }
+                return new TemplateModelListSequence(resultList);
+            } else {
+                return new LazilyGeneratedCollectionModelWithUnknownSize(
+                        new TemplateModelIterator() {
+                            boolean prefetchDone;
+                            TemplateModel prefetchedElement;
+                            boolean prefetchedEndOfIterator;
+
+                            public TemplateModel next() throws TemplateModelException {
+                                ensurePrefetchDone();
+                                if (prefetchedEndOfIterator) {
+                                    throw new IllegalStateException("next() was called when hasNext() is false");
+                                }
+                                prefetchDone = false;
+                                return prefetchedElement;
+                            }
+
+                            public boolean hasNext() throws TemplateModelException {
+                                ensurePrefetchDone();
+                                return !prefetchedEndOfIterator;
+                            }
+
+                            private void ensurePrefetchDone() throws TemplateModelException {
+                                if (prefetchDone) {
+                                    return;
+                                }
+
+                                if (lhoIterator.hasNext()) {
+                                    TemplateModel element = lhoIterator.next();
+                                    boolean elementMatched;
+                                    try {
+                                        elementMatched = elementMatches(element, elementTransformer, env);
+                                    } catch (TemplateException e) {
+                                        throw new _TemplateModelException(e, env, "Failed to transform element");
+                                    }
+                                    if (elementMatched) {
+                                        prefetchedElement = element;
+                                    } else {
+                                        prefetchedEndOfIterator = true;
+                                        prefetchedElement = null;
+                                    }
+                                } else {
+                                    prefetchedEndOfIterator = true;
+                                    prefetchedElement = null;
+                                }
+                                prefetchDone = true;
+                            }
+                        },
+                        lhoIsSequence
+                );
             }
-            return ((TemplateBooleanModel) transformedElement).getAsBoolean();
         }
 
     }
@@ -1147,6 +1225,95 @@ class BuiltInsForSequences {
 
     }
 
+    static class drop_whileBI extends FilterLikeBI {
+
+        protected TemplateModel calculateResult(
+                final TemplateModelIterator lhoIterator, final TemplateModel lho,
+                boolean lhoIsSequence, final ElementTransformer elementTransformer,
+                final Environment env) throws TemplateException {
+            if (!isLazilyGeneratedResultEnabled()) {
+                if (!lhoIsSequence) {
+                    throw _MessageUtil.newLazilyGeneratedCollectionMustBeSequenceException(drop_whileBI.this);
+                }
+
+                List<TemplateModel> resultList = new ArrayList<TemplateModel>();
+                while (lhoIterator.hasNext()) {
+                    TemplateModel element = lhoIterator.next();
+                    if (!elementMatches(element, elementTransformer, env)) {
+                        resultList.add(element);
+                        while (lhoIterator.hasNext()) {
+                            resultList.add(lhoIterator.next());
+                        }
+                        break;
+                    }
+                }
+                return new TemplateModelListSequence(resultList);
+            } else {
+                return new LazilyGeneratedCollectionModelWithUnknownSize(
+                        new TemplateModelIterator() {
+                            boolean dropMode = true;
+                            boolean prefetchDone;
+                            TemplateModel prefetchedElement;
+                            boolean prefetchedEndOfIterator;
+
+                            public TemplateModel next() throws TemplateModelException {
+                                ensurePrefetchDone();
+                                if (prefetchedEndOfIterator) {
+                                    throw new IllegalStateException("next() was called when hasNext() is false");
+                                }
+                                prefetchDone = false;
+                                return prefetchedElement;
+                            }
+
+                            public boolean hasNext() throws TemplateModelException {
+                                ensurePrefetchDone();
+                                return !prefetchedEndOfIterator;
+                            }
+
+                            private void ensurePrefetchDone() throws TemplateModelException {
+                                if (prefetchDone) {
+                                    return;
+                                }
+
+                                if (dropMode) {
+                                    boolean foundElement = false;
+                                    dropElements: while  (lhoIterator.hasNext()) {
+                                        TemplateModel element = lhoIterator.next();
+                                        try {
+                                            if (!elementMatches(element, elementTransformer, env)) {
+                                                prefetchedElement = element;
+                                                foundElement = true;
+                                                break dropElements;
+                                            }
+                                        } catch (TemplateException e) {
+                                            throw new _TemplateModelException(e, env,
+                                                    "Failed to transform element");
+                                        }
+                                    }
+                                    dropMode = false;
+                                    if (!foundElement) {
+                                        prefetchedEndOfIterator = true;
+                                        prefetchedElement = null;
+                                    }
+                                } else {
+                                    if (lhoIterator.hasNext()) {
+                                        TemplateModel element = lhoIterator.next();
+                                        prefetchedElement = element;
+                                    } else {
+                                        prefetchedEndOfIterator = true;
+                                        prefetchedElement = null;
+                                    }
+                                }
+                                prefetchDone = true;
+                            }
+                        },
+                        lhoIsSequence
+                );
+            }
+        }
+
+    }
+
     // Can't be instantiated
     private BuiltInsForSequences() { }
 
diff --git a/src/main/java/freemarker/core/_MessageUtil.java b/src/main/java/freemarker/core/_MessageUtil.java
index 4d2df6a..ebbac10 100644
--- a/src/main/java/freemarker/core/_MessageUtil.java
+++ b/src/main/java/freemarker/core/_MessageUtil.java
@@ -342,6 +342,12 @@ public class _MessageUtil {
                         ", which leads to this restriction."));
     }
 
+    /**
+     * Because of the limitations of FTL lambdas (called "local lambdas"), sometimes we must condense the lazy result
+     * down into a sequence. However, doing that automatically is only allowed if the input was a sequence as well. If
+     * it wasn't a sequence, we don't dare to collect the result into a sequence automatically (because it's possibly
+     * too long), and that's when this error message comes.
+     */
     public static TemplateException newLazilyGeneratedCollectionMustBeSequenceException(Expression blamed) {
         return new _MiscTemplateException(blamed,
                 "The result is a listable value with lazy transformation(s) applied on it, but it's not " +
diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml
index 931f768..ec49d8e 100644
--- a/src/manual/en_US/book.xml
+++ b/src/manual/en_US/book.xml
@@ -20,11 +20,7 @@
 <book conformance="docgen" version="5.0" xml:lang="en"
       xmlns="http://docbook.org/ns/docbook"
       xmlns:xlink="http://www.w3.org/1999/xlink"
-      xmlns:xi="http://www.w3.org/2001/XInclude"
-      xmlns:ns5="http://www.w3.org/1999/xhtml"
-      xmlns:ns4="http://www.w3.org/2000/svg"
-      xmlns:ns3="http://www.w3.org/1998/Math/MathML"
-      xmlns:ns="http://docbook.org/ns/docbook">
+>
   <info>
     <title>Apache FreeMarker Manual</title>
 
@@ -27900,8 +27896,10 @@ TemplateModel x = env.getVariable("x");  // get variable x</programlisting>
           <itemizedlist>
             <listitem>
               <para>Added new built-ins:
-              <literal>?filter(<replaceable>predicate</replaceable>)</literal>
-              and <literal>?map(<replaceable>mapper</replaceable>)</literal>.
+              <literal>?filter(<replaceable>predicate</replaceable>)</literal>,
+              <literal>?map(<replaceable>mapper</replaceable>)</literal>,
+              <literal>?take_while(<replaceable>predicate</replaceable>)</literal>,
+              <literal>?drop_while(<replaceable>predicate</replaceable>)</literal>.
               These allow using lambda expression, like
               <literal>users?filter(user -&gt; user.superuser)</literal> or
               <literal>users?map(user -&gt; user.name)</literal>, or accept a
diff --git a/src/test/java/freemarker/core/NullTransparencyTest.java b/src/test/java/freemarker/core/NullTransparencyTest.java
index bde9957..a5f6638 100644
--- a/src/test/java/freemarker/core/NullTransparencyTest.java
+++ b/src/test/java/freemarker/core/NullTransparencyTest.java
@@ -56,7 +56,6 @@ public class NullTransparencyTest extends TemplateTest {
 
     @Test
     public void testWithoutClashingHigherScopeVar() throws Exception {
-
         assertTrue(getConfiguration().getFallbackOnNullLoopVariable());
         testLambdaArguments();
         testLoopVariables("null");
@@ -83,6 +82,8 @@ public class NullTransparencyTest extends TemplateTest {
     protected void testLambdaArguments() throws IOException, TemplateException {
         assertOutput("<#list list?filter(it -> it??) as it>${it!'null'}<#sep>, </#list>",
                 "a, b");
+        assertOutput("<#list list?takeWhile(it -> it??) as it>${it!'null'}<#sep>, </#list>",
+                "a");
         assertOutput("<#list list?map(it -> it!'null') as it>${it}<#sep>, </#list>",
                 "a, null, b");
     }
diff --git a/src/test/java/freemarker/core/TakeWhileAndDropWhileBiTest.java b/src/test/java/freemarker/core/TakeWhileAndDropWhileBiTest.java
new file mode 100644
index 0000000..b55e85d
--- /dev/null
+++ b/src/test/java/freemarker/core/TakeWhileAndDropWhileBiTest.java
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.core;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+
+import freemarker.template.Configuration;
+import freemarker.template.DefaultObjectWrapper;
+import freemarker.test.TemplateTest;
+
+public class TakeWhileAndDropWhileBiTest extends TemplateTest {
+
+    private static class TestParam {
+        private final List<?> list;
+        private final String takeWhileResult;
+        private final String dropWhileResult;
+
+        public TestParam(List<?> list, String takeWhileResult, String dropWhileResult) {
+            this.list = list;
+            this.takeWhileResult = takeWhileResult;
+            this.dropWhileResult = dropWhileResult;
+        }
+    }
+
+    @Override
+    protected Configuration createConfiguration() throws Exception {
+        Configuration cfg = super.createConfiguration();
+
+        DefaultObjectWrapper objectWrapper = new DefaultObjectWrapper(Configuration.VERSION_2_3_28);
+        objectWrapper.setForceLegacyNonListCollections(false);
+        cfg.setObjectWrapper(objectWrapper);
+
+        return cfg;
+    }
+
+    private static final List<TestParam> TEST_PARAMS = ImmutableList.of(
+            new TestParam(ImmutableList.of(),
+                    "",
+                    ""),
+            new TestParam(ImmutableList.of("a"),
+                    "a",
+                    "a"),
+            new TestParam(ImmutableList.of("a", "b", "c"),
+                    "a, b, c",
+                    "a, b, c"),
+            new TestParam(ImmutableList.of("aX"),
+                    "",
+                    ""),
+            new TestParam(ImmutableList.of("aX", "b"),
+                    "",
+                    "b"),
+            new TestParam(ImmutableList.of("aX", "b", "c"),
+                    "",
+                    "b, c"),
+            new TestParam(ImmutableList.of("a", "bX", "c"),
+                    "a",
+                    "a, bX, c"),
+            new TestParam(ImmutableList.of("a", "b", "cX"),
+                    "a, b",
+                    "a, b, cX"),
+            new TestParam(ImmutableList.of("aX", "bX", "c"),
+                    "",
+                    "c"),
+            new TestParam(ImmutableList.of("aX", "bX", "cX"),
+                    "",
+                    ""),
+            new TestParam(ImmutableList.of("aX", "b", "cX"),
+                    "",
+                    "b, cX")
+    );
+
+    @Test
+    public void testTakeWhile() throws Exception {
+        for (TestParam testParam : TEST_PARAMS) {
+            addToDataModel("xs", testParam.list);
+            assertOutput(
+                    "<#list xs?takeWhile(it -> !it?contains('X')) as x>${x}<#sep>, </#list>",
+                    testParam.takeWhileResult);
+            assertOutput(
+                    "<#assign fxs = xs?takeWhile(it -> !it?contains('X'))>" +
+                            "${fxs?join(', ')}",
+                    testParam.takeWhileResult);
+        }
+    }
+
+    @Test
+    public void testDropWhile() throws Exception {
+        for (TestParam testParam : TEST_PARAMS) {
+            addToDataModel("xs", testParam.list);
+            assertOutput(
+                    "<#list xs?dropWhile(it -> it?contains('X')) as x>${x}<#sep>, </#list>",
+                    testParam.dropWhileResult);
+            assertOutput(
+                    "<#assign fxs = xs?dropWhile(it -> it?contains('X'))>" +
+                            "${fxs?join(', ')}",
+                    testParam.dropWhileResult);
+        }
+    }
+
+    // Chaining the two built-ins is not a special case, but, in the hope of running into some bugs, we test that too.
+    @Test
+    public void testBetween() throws Exception {
+        String ftl = "<#list xs?dropWhile(it -> it < 0)?takeWhile(it -> it >= 0) as x>${x}<#sep>, </#list>";
+
+        addToDataModel("xs", ImmutableList.of(-1, -2, 3, 4, -5, -6));
+        assertOutput(ftl,  "3, 4");
+
+        addToDataModel("xs", ImmutableList.of(-1, -2, -5, -6));
+        assertOutput(ftl,  "");
+
+        addToDataModel("xs", ImmutableList.of(1, 2, 3));
+        assertOutput(ftl,  "1, 2, 3");
+
+        addToDataModel("xs", Collections.emptyList());
+        assertOutput(ftl,  "");
+    }
+
+    @Test
+    public void testSnakeCaseNames() throws Exception {
+        addToDataModel("xs", ImmutableList.of(-1, -2, 3, 4, -5, -6));
+        assertOutput(
+                "<#list xs?drop_while(it -> it < 0)?take_while(it -> it >= 0) as x>${x}<#sep>, </#list>",
+                "3, 4");
+    }
+
+}
\ No newline at end of file