ConcatenatedSequence now also implements TemplateCollectionModelEx, and thus has an isEmpty() that's more efficient than size() == 0. Also extende the Unit test to check with more kind sequence implementations, and with sequences containing null items.
diff --git a/src/main/java/freemarker/core/AddConcatExpression.java b/src/main/java/freemarker/core/AddConcatExpression.java
index dba2d4e..d2c5745 100644
--- a/src/main/java/freemarker/core/AddConcatExpression.java
+++ b/src/main/java/freemarker/core/AddConcatExpression.java
@@ -29,6 +29,7 @@
import freemarker.template.SimpleScalar;
import freemarker.template.SimpleSequence;
import freemarker.template.TemplateCollectionModel;
+import freemarker.template.TemplateCollectionModelEx;
import freemarker.template.TemplateException;
import freemarker.template.TemplateHashModel;
import freemarker.template.TemplateHashModelEx;
@@ -197,7 +198,7 @@
// Non-private for unit testing
static final class ConcatenatedSequence
implements
- TemplateSequenceModel, TemplateCollectionModel {
+ TemplateSequenceModel, TemplateCollectionModelEx {
private final TemplateSequenceModel left;
private final TemplateSequenceModel right;
@@ -244,6 +245,50 @@
}
@Override
+ public boolean isEmpty() throws TemplateModelException {
+ ConcatenatedSequence[] concSeqsWithRightPending = new ConcatenatedSequence[2];
+ int concSeqsWithRightPendingLength = 0;
+ ConcatenatedSequence concSeqInFocus = this;
+
+ while (true) {
+ TemplateSequenceModel left;
+ while ((left = concSeqInFocus.left) instanceof ConcatenatedSequence) {
+ if (concSeqsWithRightPendingLength == concSeqsWithRightPending.length) {
+ concSeqsWithRightPending = Arrays.copyOf(concSeqsWithRightPending, concSeqsWithRightPendingLength * 2);
+ }
+ concSeqsWithRightPending[concSeqsWithRightPendingLength++] = concSeqInFocus;
+ concSeqInFocus = (ConcatenatedSequence) left;
+ }
+ if (!isEmpty(left)) {
+ return false;
+ }
+
+ while (true) {
+ TemplateSequenceModel right = concSeqInFocus.right;
+ if (right instanceof ConcatenatedSequence) {
+ concSeqInFocus = (ConcatenatedSequence) right;
+ break; // To jump at the left-descending loop
+ }
+ if (!isEmpty(right)) {
+ return false;
+ }
+
+ if (concSeqsWithRightPendingLength == 0) {
+ return true;
+ }
+
+ concSeqsWithRightPendingLength--;
+ concSeqInFocus = concSeqsWithRightPending[concSeqsWithRightPendingLength];
+ }
+ }
+ }
+
+ private static boolean isEmpty(TemplateSequenceModel seq) throws TemplateModelException {
+ return seq instanceof TemplateCollectionModelEx ? ((TemplateCollectionModelEx) seq).isEmpty()
+ : seq.size() == 0;
+ }
+
+ @Override
public TemplateModel get(int index) throws TemplateModelException {
if (index < 0) {
return null;
diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml
index e164e38..7ba7d31 100644
--- a/src/manual/en_US/book.xml
+++ b/src/manual/en_US/book.xml
@@ -30176,7 +30176,7 @@
<literal>List</literal>-s) with the <literal>+</literal>
operation in templates, the resulting
<literal>TemplateSequenceModel</literal> now also implements
- <literal>TemplateCollectionModel</literal> (with is similar to
+ <literal>TemplateCollectionModelEx</literal> (with is similar to
Java's <literal>Iterable</literal>). It's because using that is
much more efficient then indexed access, if the sequence was
concatenated together from a lot of sequences.</para>
diff --git a/src/test/java/freemarker/core/ConcatenatedSequenceTest.java b/src/test/java/freemarker/core/ConcatenatedSequenceTest.java
index f36af67..8bc2137 100644
--- a/src/test/java/freemarker/core/ConcatenatedSequenceTest.java
+++ b/src/test/java/freemarker/core/ConcatenatedSequenceTest.java
@@ -23,14 +23,23 @@
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Iterator;
import java.util.List;
+import java.util.NoSuchElementException;
import java.util.function.Supplier;
import org.junit.Test;
import freemarker.core.AddConcatExpression.ConcatenatedSequence;
+import freemarker.template.Configuration;
+import freemarker.template.DefaultListAdapter;
+import freemarker.template.DefaultObjectWrapper;
+import freemarker.template.DefaultObjectWrapperBuilder;
import freemarker.template.SimpleCollection;
+import freemarker.template.SimpleScalar;
import freemarker.template.SimpleSequence;
+import freemarker.template.TemplateCollectionModelEx;
+import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateModelIterator;
import freemarker.template.TemplateScalarModel;
@@ -43,11 +52,42 @@
}
@Test
- public void testForSequences() throws TemplateModelException {
- test(new SeqFactory() {
+ public void testForSimpleSequences() throws TemplateModelException {
+ testWithSegmentFactory(new SeqFactory() {
@Override
public TemplateSequenceModel create(String... items) {
- return new SimpleSequence(List.of(items));
+ return new SimpleSequence(Arrays.asList(items));
+ }
+
+ @Override
+ public boolean isUnrepeatable() {
+ return false;
+ }
+ });
+ }
+
+ @Test
+ public void testForListAdapter() throws TemplateModelException {
+ DefaultObjectWrapper objectWrapper = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_32).build();
+ testWithSegmentFactory(new SeqFactory() {
+ @Override
+ public TemplateSequenceModel create(String... items) {
+ return DefaultListAdapter.adapt(Arrays.asList(items), objectWrapper);
+ }
+
+ @Override
+ public boolean isUnrepeatable() {
+ return false;
+ }
+ });
+ }
+
+ @Test
+ public void testForSequenceAndCollectionModelEx() throws TemplateModelException {
+ testWithSegmentFactory(new SeqFactory() {
+ @Override
+ public TemplateSequenceModel create(String... items) {
+ return new SequenceAndCollectionModelEx(Arrays.asList(items));
}
@Override
@@ -59,7 +99,7 @@
@Test
public void testForCollectionsWrappingIterable() throws TemplateModelException {
- test(new SeqFactory() {
+ testWithSegmentFactory(new SeqFactory() {
@Override
public TemplateSequenceModel create(String... items) {
return new CollectionAndSequence(new SimpleCollection(Arrays.asList(items)));
@@ -74,7 +114,7 @@
@Test
public void testForCollectionsWrappingIterator() throws TemplateModelException {
- test(new SeqFactory() {
+ testWithSegmentFactory(new SeqFactory() {
@Override
public TemplateSequenceModel create(String... items) {
return new CollectionAndSequence(new SimpleCollection(Arrays.asList(items).iterator()));
@@ -87,7 +127,7 @@
});
}
- public void test(SeqFactory segmentFactory) throws TemplateModelException {
+ public void testWithSegmentFactory(SeqFactory segmentFactory) throws TemplateModelException {
assertConcatenationResult(segmentFactory.isUnrepeatable(), () ->
new ConcatenatedSequence(segmentFactory.create(), segmentFactory.create()));
assertConcatenationResult(segmentFactory.isUnrepeatable(), () ->
@@ -99,6 +139,94 @@
assertConcatenationResult(segmentFactory.isUnrepeatable(), () ->
new ConcatenatedSequence(segmentFactory.create("a"), segmentFactory.create("b")),
"a", "b");
+ assertConcatenationResult(segmentFactory.isUnrepeatable(), () ->
+ new ConcatenatedSequence(
+ new ConcatenatedSequence(
+ segmentFactory.create(),
+ segmentFactory.create()),
+ new ConcatenatedSequence(
+ segmentFactory.create(),
+ segmentFactory.create())
+ )
+ );
+
+ assertConcatenationResult(segmentFactory.isUnrepeatable(), () ->
+ new ConcatenatedSequence(
+ new ConcatenatedSequence(
+ segmentFactory.create("a", "b"),
+ segmentFactory.create()),
+ new ConcatenatedSequence(
+ segmentFactory.create(),
+ segmentFactory.create())
+ ),
+ "a", "b"
+ );
+ assertConcatenationResult(segmentFactory.isUnrepeatable(), () ->
+ new ConcatenatedSequence(
+ new ConcatenatedSequence(
+ segmentFactory.create(),
+ segmentFactory.create("a", "b")),
+ new ConcatenatedSequence(
+ segmentFactory.create(),
+ segmentFactory.create())
+ ),
+ "a", "b"
+ );
+ assertConcatenationResult(segmentFactory.isUnrepeatable(), () ->
+ new ConcatenatedSequence(
+ new ConcatenatedSequence(
+ segmentFactory.create(),
+ segmentFactory.create()),
+ new ConcatenatedSequence(
+ segmentFactory.create("a", "b"),
+ segmentFactory.create())
+ ),
+ "a", "b"
+ );
+ assertConcatenationResult(segmentFactory.isUnrepeatable(), () ->
+ new ConcatenatedSequence(
+ new ConcatenatedSequence(
+ segmentFactory.create(),
+ segmentFactory.create()),
+ new ConcatenatedSequence(
+ segmentFactory.create(),
+ segmentFactory.create("a", "b"))
+ ),
+ "a", "b"
+ );
+ assertConcatenationResult(segmentFactory.isUnrepeatable(), () ->
+ new ConcatenatedSequence(
+ new ConcatenatedSequence(
+ segmentFactory.create("a"),
+ segmentFactory.create("b")),
+ new ConcatenatedSequence(
+ segmentFactory.create(),
+ segmentFactory.create())
+ ),
+ "a", "b"
+ );
+ assertConcatenationResult(segmentFactory.isUnrepeatable(), () ->
+ new ConcatenatedSequence(
+ new ConcatenatedSequence(
+ segmentFactory.create(),
+ segmentFactory.create("a")),
+ new ConcatenatedSequence(
+ segmentFactory.create("b"),
+ segmentFactory.create())
+ ),
+ "a", "b"
+ );
+ assertConcatenationResult(segmentFactory.isUnrepeatable(), () ->
+ new ConcatenatedSequence(
+ new ConcatenatedSequence(
+ segmentFactory.create(),
+ segmentFactory.create()),
+ new ConcatenatedSequence(
+ segmentFactory.create("a"),
+ segmentFactory.create("b"))
+ ),
+ "a", "b"
+ );
assertConcatenationResult(segmentFactory.isUnrepeatable(), () ->
new ConcatenatedSequence(
@@ -169,6 +297,15 @@
},
"a", "b", "a", "b", "a", "b", "a", "b");
}
+
+ assertConcatenationResult(segmentFactory.isUnrepeatable(), () ->
+ new ConcatenatedSequence(
+ new ConcatenatedSequence(
+ segmentFactory.create(null, "a"),
+ segmentFactory.create("b", null)),
+ segmentFactory.create((String) null)
+ ),
+ null, "a", "b", null, null);
}
private void assertConcatenationResult(
@@ -181,7 +318,7 @@
{
List<String> actualItems = new ArrayList<>();
for (TemplateModelIterator iter = seq.iterator(); iter.hasNext(); ) {
- actualItems.add(((TemplateScalarModel) iter.next()).getAsString());
+ actualItems.add(asNullableString((TemplateScalarModel) iter.next()));
}
assertEquals(Arrays.asList(expectedItems), actualItems);
}
@@ -194,7 +331,7 @@
List<String> actualItems = new ArrayList<>();
for (TemplateModelIterator iter = seq.iterator(); iter.hasNext(); ) {
assertTrue(iter.hasNext());
- actualItems.add(((TemplateScalarModel) iter.next()).getAsString());
+ actualItems.add(asNullableString((TemplateScalarModel) iter.next()));
}
assertEquals(Arrays.asList(expectedItems), actualItems);
}
@@ -207,7 +344,7 @@
List<String> actualItems = new ArrayList<>();
int size = seq.size();
for (int i = 0; i < size; i++) {
- actualItems.add(((TemplateScalarModel) seq.get(i)).getAsString());
+ actualItems.add(asNullableString((TemplateScalarModel) seq.get(i)));
}
assertEquals(Arrays.asList(expectedItems), actualItems);
assertNull(seq.get(-1));
@@ -220,6 +357,65 @@
}
assertEquals(expectedItems.length, seq.size());
+
+ if (repeatable) {
+ seq = seqSupplier.get();
+ }
+
+ assertEquals(expectedItems.length == 0, seq.isEmpty());
+ }
+
+ private String asNullableString(TemplateScalarModel model) throws TemplateModelException {
+ return model != null ? model.getAsString() : null;
+ }
+
+ /**
+ * This is to test {@link TemplateSequenceModel} that's also a {@link TemplateCollectionModelEx}.
+ */
+ private static class SequenceAndCollectionModelEx implements TemplateSequenceModel, TemplateCollectionModelEx {
+ private final List<String> items;
+
+ public SequenceAndCollectionModelEx(List<String> items) {
+ this.items = items;
+ }
+
+ @Override
+ public TemplateModelIterator iterator() throws TemplateModelException {
+ return new TemplateModelIterator() {
+ private final Iterator<String> it = items.iterator();
+
+ @Override
+ public TemplateModel next() throws TemplateModelException {
+ try {
+ String value = it.next();
+ return value != null ? new SimpleScalar(value) : null;
+ } catch (NoSuchElementException e) {
+ throw new TemplateModelException("The collection has no more items.", e);
+ }
+ }
+
+ @Override
+ public boolean hasNext() throws TemplateModelException {
+ return it.hasNext();
+ }
+ };
+ }
+
+ @Override
+ public boolean isEmpty() throws TemplateModelException {
+ return items.isEmpty();
+ }
+
+ @Override
+ public TemplateModel get(int index) throws TemplateModelException {
+ String value = items.get(index);
+ return value != null ? new SimpleScalar(value) : null;
+ }
+
+ @Override
+ public int size() throws TemplateModelException {
+ return items.size();
+ }
}
}
\ No newline at end of file