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));