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/05/25 23:02:17 UTC

[freemarker] 03/03: exp[rangeExp] now returns sequence or lazily generated sequence depending on what the consumer supports. Also added size bypassing through exp[rangeExp] (like in seq?map(f)[10..]?size no seq element will be consumed, as the size can be calculated without that).

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 a2ed41d561484bdcd206df689b9809ae273a7680
Author: ddekany <dd...@apache.org>
AuthorDate: Sun May 26 00:44:16 2019 +0200

    exp[rangeExp] now returns sequence or lazily generated sequence depending on what the consumer supports. Also added size bypassing through exp[rangeExp] (like in seq?map(f)[10..]?size no seq element will be consumed, as the size can be calculated without that).
---
 src/main/java/freemarker/core/DynamicKeyName.java  | 145 +++++++++++++--------
 .../core/LazilyGeneratedSequenceModelWithSize.java |  41 ++++++
 .../freemarker/core/LazilyGeneratedSeqTest.java    |  47 ++++++-
 3 files changed, 177 insertions(+), 56 deletions(-)

diff --git a/src/main/java/freemarker/core/DynamicKeyName.java b/src/main/java/freemarker/core/DynamicKeyName.java
index 15ff96b..7b57983 100644
--- a/src/main/java/freemarker/core/DynamicKeyName.java
+++ b/src/main/java/freemarker/core/DynamicKeyName.java
@@ -20,7 +20,9 @@
 package freemarker.core;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 
 import freemarker.template.SimpleScalar;
 import freemarker.template.SimpleSequence;
@@ -46,6 +48,7 @@ final class DynamicKeyName extends Expression {
 
     private final Expression keyExpression;
     private final Expression target;
+    private boolean lazilyGeneratedResultEnabled;
 
     DynamicKeyName(Expression target, Expression keyExpression) {
         this.target = target; 
@@ -162,7 +165,7 @@ final class DynamicKeyName extends Expression {
 
     private TemplateModel dealWithRangeKey(TemplateModel targetModel, RangeModel range, Environment env)
     throws TemplateException {
-        // We can have 3 kind of left hand operands ("targets"): sequence, lazyily generated sequence, string
+        // We can have 3 kind of left hand operands ("targets"): sequence, lazily generated sequence, string
         final TemplateSequenceModel targetSeq;
         final LazilyGeneratedSequenceModel targetLazySeq;
         final String targetStr;
@@ -238,8 +241,11 @@ final class DynamicKeyName extends Expression {
                         ". ", "(Note that indices are 0-based).");
             }
         }
-        
-        final int resultSize; // Might will be UNKNOWN_RESULT_SIZE, when targetLazySeq != null
+
+        // Calculate resultSize. Note that:
+        // - It might will be UNKNOWN_RESULT_SIZE when targetLazySeq != null.
+        // - It might will be out-of-bounds if !targetSizeKnown (otherwise we validate if the range is correct)
+        final int resultSize;
         if (!rightUnbounded) {
             final int lastIdx = firstIdx + (rangeSize - 1) * step; // Note: lastIdx is inclusive
             if (lastIdx < 0) {
@@ -271,6 +277,9 @@ final class DynamicKeyName extends Expression {
             return emptyResult(targetSeq != null);
         }
         if (targetSeq != null) {
+            // In principle we should take lazilyGeneratedResultEnabled into account, but that wouldn't be backward
+            // compatible. For example, with lazily generated sequence result <#list xs[b..e] as x> would behave
+            // differently if xs is modified inside the #list nested content.
             ArrayList<TemplateModel> resultList = new ArrayList<TemplateModel>(resultSize);
             int srcIdx = firstIdx;
             for (int i = 0; i < resultSize; i++) {
@@ -280,8 +289,10 @@ final class DynamicKeyName extends Expression {
             // List items are already wrapped, so the wrapper will be null:
             return new SimpleSequence(resultList, null);
         } else if (targetLazySeq != null) {
+            // As a targetLazySeq can only occur if a new built-in like ?filter or ?map was used somewhere in the target
+            // expression, in this case we can return lazily generated sequence without breaking backward compatibility.
             if (step == 1) {
-                return getStep1RangeFromIterator(targetLazySeq.iterator(), range, resultSize);
+                return getStep1RangeFromIterator(targetLazySeq.iterator(), range, resultSize, targetSizeKnown);
             } else if (step == -1) {
                 return getStepMinus1RangeFromIterator(targetLazySeq.iterator(), range, resultSize);
             } else {
@@ -308,55 +319,88 @@ final class DynamicKeyName extends Expression {
         }
     }
 
-    private TemplateModel getStep1RangeFromIterator(final TemplateModelIterator targetIter, final RangeModel range, int resultSize)
+    private TemplateModel getStep1RangeFromIterator(
+            final TemplateModelIterator targetIter, final RangeModel range, int resultSize, boolean targetSizeKnown)
             throws TemplateModelException {
         final int firstIdx = range.getBegining();
         final int lastIdx = firstIdx + (range.size() - 1); // Note: meaningless if the range is right unbounded
         final boolean rightAdaptive = range.isRightAdaptive();
         final boolean rightUnbounded = range.isRightUnbounded();
-        return new LazilyGeneratedSequenceModel(new TemplateModelIterator() {
-            private boolean elementsBeforeFirsIndexWereSkipped;
-            private int nextIdx;
-
-            public TemplateModel next() throws TemplateModelException {
-                ensureElementsBeforeFirstIndexWereSkipped();
-                if (!rightUnbounded && nextIdx > lastIdx) {
-                    throw new _TemplateModelException(
-                            "Iterator has no more elements (at index ", Integer.valueOf(nextIdx), ")");
+        if (lazilyGeneratedResultEnabled) {
+            TemplateModelIterator iterator = new TemplateModelIterator() {
+                private boolean elementsBeforeFirsIndexWereSkipped;
+                private int nextIdx;
+
+                public TemplateModel next() throws TemplateModelException {
+                    ensureElementsBeforeFirstIndexWereSkipped();
+                    if (!rightUnbounded && nextIdx > lastIdx) {
+                        // We fail because the consumer of this iterator hasn't used hasNext() properly.
+                        throw new _TemplateModelException(
+                                "Iterator has no more elements (at index ", Integer.valueOf(nextIdx), ")");
+                    }
+                    if (!rightAdaptive && !targetIter.hasNext()) {
+                        // We fail because the range was wrong, not because of the consumer of this iterator .
+                        throw newRangeEndOutOfBoundsException(nextIdx, lastIdx);
+                    }
+                    TemplateModel result = targetIter.next();
+                    nextIdx++;
+                    return result;
                 }
-                if (!rightAdaptive && !targetIter.hasNext()) {
-                    // We fail because the range was wrong, not because this iterator was over-consumed.
-                    throw new _TemplateModelException(keyExpression,
-                            "Range end index ", Integer.valueOf(lastIdx), " is out of bounds, as sliced sequence " +
-                            "only has ", nextIdx, " elements.");
+
+                public boolean hasNext() throws TemplateModelException {
+                    ensureElementsBeforeFirstIndexWereSkipped();
+                    return (rightUnbounded || nextIdx <= lastIdx) && (!rightAdaptive || targetIter.hasNext());
                 }
-                TemplateModel result = targetIter.next();
-                nextIdx++;
-                return result;
-            }
 
-            public boolean hasNext() throws TemplateModelException {
-                ensureElementsBeforeFirstIndexWereSkipped();
-                return (rightUnbounded || nextIdx <= lastIdx) && (!rightAdaptive || targetIter.hasNext());
-            }
+                public void ensureElementsBeforeFirstIndexWereSkipped() throws TemplateModelException {
+                    if (elementsBeforeFirsIndexWereSkipped) {
+                        return;
+                    }
 
-            public void ensureElementsBeforeFirstIndexWereSkipped() throws TemplateModelException {
-                if (elementsBeforeFirsIndexWereSkipped) {
-                    return;
+                    skipElementsBeforeFirstIndex(targetIter, firstIdx);
+                    nextIdx = firstIdx;
+                    elementsBeforeFirsIndexWereSkipped = true;
                 }
-
-                while (nextIdx < firstIdx) {
-                    if (!targetIter.hasNext()) {
-                        throw new _TemplateModelException(keyExpression,
-                                "Range start index ", Integer.valueOf(firstIdx), " is out of bounds, as the sliced " +
-                                "sequence only has ", nextIdx, " elements.");
+            };
+            return resultSize != UNKNOWN_RESULT_SIZE && targetSizeKnown  // targetSizeKnown => range end was validated
+                    ? new LazilyGeneratedSequenceModelWithSize(iterator, resultSize)
+                    : new LazilyGeneratedSequenceModel(iterator);
+        } else { // !lazilyGeneratedResultEnabled
+            List<TemplateModel> resultList = resultSize != UNKNOWN_RESULT_SIZE
+                    ? new ArrayList<TemplateModel>(resultSize)
+                    : new ArrayList<TemplateModel>();
+            skipElementsBeforeFirstIndex(targetIter, firstIdx);
+            collectElements: for (int nextIdx = firstIdx; rightUnbounded || nextIdx <= lastIdx; nextIdx++) {
+                if (!targetIter.hasNext()) {
+                    if (!rightAdaptive) {
+                        throw newRangeEndOutOfBoundsException(nextIdx, lastIdx);
                     }
-                    targetIter.next();
-                    nextIdx++;
+                    break collectElements;
                 }
-                elementsBeforeFirsIndexWereSkipped = true;
+                resultList.add(targetIter.next());
             }
-        });
+            // List items are already wrapped, so the wrapper will be null:
+            return new SimpleSequence(resultList, null);
+        }
+    }
+
+    private void skipElementsBeforeFirstIndex(TemplateModelIterator targetIter, int firstIdx) throws TemplateModelException {
+        int nextIdx = 0;
+        while (nextIdx < firstIdx) {
+            if (!targetIter.hasNext()) {
+                throw new _TemplateModelException(keyExpression,
+                        "Range start index ", Integer.valueOf(firstIdx),
+                        " is out of bounds, as the sliced sequence only has ", nextIdx, " elements.");
+            }
+            targetIter.next();
+            nextIdx++;
+        }
+    }
+
+    private _TemplateModelException newRangeEndOutOfBoundsException(int nextIdx, int lastIdx) {
+        return new _TemplateModelException(keyExpression,
+                "Range end index ", Integer.valueOf(lastIdx), " is out of bounds, as sliced sequence " +
+                "only has ", nextIdx, " elements.");
     }
 
     // Because the order has to be reversed, we have to "buffer" the stream in this case.
@@ -383,21 +427,7 @@ final class DynamicKeyName extends Expression {
                     "Range top index " + highIndex + " (0-based) is outside the sliced sequence of length " +
                     srcIdx + ".");
         }
-        return new LazilyGeneratedSequenceModel(new TemplateModelIterator() {
-            private int nextIndex;
-
-            public TemplateModel next() throws TemplateModelException {
-                try {
-                    return resultElements[nextIndex++];
-                } catch (IndexOutOfBoundsException e) {
-                    throw new TemplateModelException("There are no more elements in the iterator");
-                }
-            }
-
-            public boolean hasNext() throws TemplateModelException {
-                return nextIndex < resultElements.length;
-            }
-        });
+        return new SimpleSequence(Arrays.asList(resultElements), null);
     }
 
     private TemplateModel emptyResult(boolean seq) {
@@ -409,6 +439,11 @@ final class DynamicKeyName extends Expression {
     }
 
     @Override
+    void enableLazilyGeneratedResult() {
+        lazilyGeneratedResultEnabled = true;
+    }
+
+    @Override
     public String getCanonicalForm() {
         return target.getCanonicalForm() 
                + "[" 
diff --git a/src/main/java/freemarker/core/LazilyGeneratedSequenceModelWithSize.java b/src/main/java/freemarker/core/LazilyGeneratedSequenceModelWithSize.java
new file mode 100644
index 0000000..7faab2e
--- /dev/null
+++ b/src/main/java/freemarker/core/LazilyGeneratedSequenceModelWithSize.java
@@ -0,0 +1,41 @@
+/*
+ * 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 freemarker.template.TemplateCollectionModelEx;
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateModelIterator;
+
+class LazilyGeneratedSequenceModelWithSize extends LazilyGeneratedSequenceModel implements TemplateCollectionModelEx {
+    private final int size;
+
+    LazilyGeneratedSequenceModelWithSize(TemplateModelIterator iterator, int size) {
+        super(iterator);
+        this.size = size;
+    }
+
+    public int size() throws TemplateModelException {
+        return size;
+    }
+
+    public boolean isEmpty() {
+        return size == 0;
+    }
+}
diff --git a/src/test/java/freemarker/core/LazilyGeneratedSeqTest.java b/src/test/java/freemarker/core/LazilyGeneratedSeqTest.java
index b4618aa..0886f05 100644
--- a/src/test/java/freemarker/core/LazilyGeneratedSeqTest.java
+++ b/src/test/java/freemarker/core/LazilyGeneratedSeqTest.java
@@ -172,7 +172,51 @@ public class LazilyGeneratedSeqTest extends TemplateTest {
                 "[iterator][hasNext][next][hasNext][next]2");
     }
 
-
+    @Test
+    public void rangeOperatorTest() throws Exception {
+        assertErrorContains("${coll[1..2]?join(', ')}", "sequence", "collection");
+
+        assertOutput("${seq[1..2]?first}", "[size][get 1][get 2]2");
+        assertOutput("${seq[1..]?first}",  "[size][get 1][get 2]2");
+        assertOutput("${seq[2..1]?first}",  "[size][get 2][get 1]3");
+
+        assertOutput("${seqLong[1..3]?first}", "[size][get 1][get 2][get 3]2");
+        assertOutput("${seqLong[1..]?first}",  "[size][get 1][get 2][get 3][get 4][get 5]2");
+        assertOutput("${seqLong[3..1]?first}",  "[size][get 3][get 2][get 1]4");
+
+        assertOutput("${seq?map(x->x)[1..2]?first}", "[size][size][get 0][get 1]2");
+        assertOutput("${seq?map(x->x)[1..]?first}",  "[size][size][get 0][get 1]2");
+        // Why 2 [size]-s above: 1st to validate range. 2nd for the 1st hasNext call on the iterator.
+        assertOutput("${seq?map(x->x)[2..1]?first}",  "[size][size][get 0][get 1][get 2]3");
+
+        assertOutput("${seqLong?map(x->x)[1..3]?first}", "[size][size][get 0][get 1]2");
+        assertOutput("${seqLong?map(x->x)[1..]?first}",  "[size][size][get 0][get 1]2");
+        assertOutput("${seqLong?map(x->x)[3..1]?first}",  "[size][size][get 0][get 1][get 2][get 3]4");
+
+        assertOutput("${seq?filter(x->true)[1..2]?first}", "[size][get 0][get 1]2");
+        assertOutput("${seq?filter(x->true)[1..]?first}",  "[size][get 0][get 1]2");
+        assertOutput("${seq?filter(x->true)[2..1]?first}",  "[size][get 0][get 1][get 2]3");
+
+        assertOutput("${seqLong?filter(x->true)[1..3]?first}", "[size][get 0][get 1]2");
+        assertOutput("${seqLong?filter(x->true)[1..]?first}",  "[size][get 0][get 1]2");
+        assertOutput("${seqLong?filter(x->true)[3..1]?first}",  "[size][get 0][get 1][get 2][get 3]4");
+
+        assertOutput("${seq[1..2][0..1]?first}", "[size][get 1][get 2]2");
+        assertOutput("${seq?map(x->x)[1..2][0..1]?first}", "[size][size][get 0][get 1]2");
+        assertOutput("${seq?filter(x->true)[1..2][0..1]?first}", "[size][get 0][get 1]2");
+
+        assertOutput("<#list seqLong?filter(x->true)[1..3] as it>${it}</#list>",
+                "[size][get 0][get 1]2[get 2]3[get 3]4");
+        assertOutput("<#list seqLong[1..3] as it>${it}</#list>",
+                "[size][get 1][get 2][get 3]234");
+
+        assertOutput("${seq?map(x->x)[1..2]?size}", "[size]2");
+        assertOutput("${seq?filter(x->true)[1..2]?size}", "[size][get 0][get 1][get 2]2");
+        assertOutput("${seqLong?map(x->x)[2..]?size}", "[size]4");
+        assertOutput("${seqLong?filter(x->true)[2..]?size}", "[size][get 0][get 1][get 2][get 3][get 4][get 5]4");
+        assertOutput("${seqLong?map(x->x)[2..*3]?size}", "[size]3");
+        assertOutput("${seqLong?filter(x->true)[2..*3]?size}", "[size][get 0][get 1][get 2][get 3][get 4]3");
+    }
 
     @Override
     protected Configuration createConfiguration() throws Exception {
@@ -180,6 +224,7 @@ public class LazilyGeneratedSeqTest extends TemplateTest {
         cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_29);
         cfg.setBooleanFormat("c");
         cfg.setSharedVariable("seq", new MonitoredTemplateSequenceModel(1, 2, 3));
+        cfg.setSharedVariable("seqLong", new MonitoredTemplateSequenceModel(1, 2, 3, 4, 5, 6));
         cfg.setSharedVariable("coll", new MonitoredTemplateCollectionModel(1, 2, 3));
         cfg.setSharedVariable("collLong", new MonitoredTemplateCollectionModel(1, 2, 3, 4, 5, 6));
         cfg.setSharedVariable("collEx", new MonitoredTemplateCollectionModelEx(1, 2, 3));