freemarker-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ddek...@apache.org
Subject [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).
Date Sat, 25 May 2019 23:02:17 GMT
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 <ddekany@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));


Mime
View raw message