blob: 2004305b2f4d3d1950f2e35c98b3242496d62914 [file] [log] [blame]
/*
* 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.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import org.junit.Test;
import freemarker.template.Configuration;
import freemarker.template.DefaultObjectWrapper;
import freemarker.template.SimpleNumber;
import freemarker.template.TemplateCollectionModel;
import freemarker.template.TemplateCollectionModelEx;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateModelIterator;
import freemarker.template.TemplateSequenceModel;
import freemarker.test.TemplateTest;
/**
* Tests operators and built-ins that are support getting and/or returning {@link LazilyGeneratedCollectionModel}.
* @see MapBiTest
* @see FilterBiTest
*/
public class LazilyGeneratedCollectionTest extends TemplateTest {
@Override
protected Configuration createConfiguration() throws Exception {
Configuration cfg = super.createConfiguration();
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));
DefaultObjectWrapper objectWrapper = new DefaultObjectWrapper(Configuration.VERSION_2_3_28);
objectWrapper.setForceLegacyNonListCollections(false);
cfg.setObjectWrapper(objectWrapper);
return cfg;
}
@Test
public void dynamicIndexTest() throws Exception {
assertErrorContains("${coll?sequence?map(it -> it)['x']}",
"hash", "evaluated to a sequence");
assertOutput("${coll?sequence?map(it -> it)[0]}",
"[iterator][hasNext][next]1");
assertOutput("${coll?sequence?map(it -> it)[1]}",
"[iterator][hasNext][next][hasNext][next]2");
assertOutput("${coll?map(it -> it)?sequence[1]}",
"[iterator][hasNext][next][hasNext][next]2");
assertOutput("${coll?sequence?map(it -> it)[2]}",
"[iterator][hasNext][next][hasNext][next][hasNext][next]3");
assertOutput("${coll?sequence?map(it -> it)[3]!'missing'}",
"[iterator][hasNext][next][hasNext][next][hasNext][next][hasNext]missing");
assertOutput("${coll?sequence?filter(it -> it % 2 == 0)[0]}",
"[iterator][hasNext][next][hasNext][next]2");
assertOutput("${coll?sequence?filter(it -> it > 3)[0]!'missing'}",
"[iterator][hasNext][next][hasNext][next][hasNext][next][hasNext]missing");
assertOutput("${collLong?sequence?map(it -> it)[1 .. 2]?join(', ')}",
"[iterator][hasNext][next][hasNext][next][hasNext][next]2, 3");
}
@Test
public void dynamicIndexNonSequenceInput() throws Exception {
assertErrorContains("${coll[1]}", "sequence", "evaluated to a collection");
assertOutput("${coll?sequence[1]}", "[iterator][hasNext][next][hasNext][next]2");
assertErrorContains("<#assign t = coll[1..2]>", "sequence", "evaluated to a collection");
assertOutput("<#assign t = coll?sequence[1..2]>${t?join('')}",
"[iterator][hasNext][next][hasNext][next][hasNext][next]23");
assertOutput("<#list coll?sequence[1..2] as it>${it}</#list>",
"[iterator][hasNext][next][hasNext][next]2[hasNext][next]3");
}
@Test
public void sizeBasicsTest() throws Exception {
assertOutput("${seq?size}",
"[size]3");
assertOutput("${collEx?size}",
"[size]3");
assertErrorContains("${coll?size}",
"sequence", "extended collection");
assertOutput("${seq?map(x -> x * 10)?size}",
"[size]3");
assertOutput("${collEx?sequence?map(x -> x * 10)?size}",
"[size]3");
assertOutput("${collEx?map(x -> x * 10)?sequence?size}",
"[size]3");
assertOutput("${seq?filter(x -> x != 1)?size}",
"[size][get 0][get 1][get 2]2");
assertOutput("${collEx?sequence?filter(x -> x != 1)?size}",
"[iterator][hasNext][next][hasNext][next][hasNext][next][hasNext]2");
assertOutput("${collEx?filter(x -> x != 1)?sequence?size}",
"[iterator][hasNext][next][hasNext][next][hasNext][next][hasNext]2");
}
@Test
public void sizeComparisonTest() throws Exception {
// These actually aren't related to lazy generation...
assertOutput("${collEx?size}",
"[size]3");
assertOutput("${collEx?size != 0}",
"[isEmpty]true");
assertOutput("${0 != collEx?size}",
"[isEmpty]true");
assertOutput("${collEx?size == 0}",
"[isEmpty]false");
assertOutput("${0 == collEx?size}",
"[isEmpty]false");
assertOutput("${(collEx?size >= 1)}",
"[isEmpty]true");
assertOutput("${1 <= collEx?size}",
"[isEmpty]true");
assertOutput("${collEx?size <= 0}",
"[isEmpty]false");
assertOutput("${(0 >= collEx?size)}",
"[isEmpty]false");
assertOutput("${collEx?size > 0}",
"[isEmpty]true");
assertOutput("${0 < collEx?size}",
"[isEmpty]true");
assertOutput("${collEx?size < 1}",
"[isEmpty]false");
assertOutput("${1 > collEx?size}",
"[isEmpty]false");
assertOutput("${collEx?size == 1}",
"[size]false");
assertOutput("${1 == collEx?size}",
"[size]false");
// Now the lazy generation things:
assertOutput("${collLong?sequence?filter(x -> true)?size}",
"[iterator]" +
"[hasNext][next][hasNext][next][hasNext][next]" +
"[hasNext][next][hasNext][next][hasNext][next][hasNext]6");
// Note: "[next]" is added by ?filter, as it has to know if the element matches the predicate.
assertOutput("${collLong?sequence?filter(x -> true)?size != 0}",
"[iterator][hasNext][next]true");
assertOutput("${collLong?sequence?filter(x -> true)?size != 1}",
"[iterator][hasNext][next][hasNext][next]true");
assertOutput("${collLong?sequence?filter(x -> true)?size == 1}",
"[iterator][hasNext][next][hasNext][next]false");
assertOutput("${collLong?filter(x -> true)?sequence?size == 1}",
"[iterator][hasNext][next][hasNext][next]false");
assertOutput("${collLong?sequence?filter(x -> true)?size < 3}",
"[iterator][hasNext][next][hasNext][next][hasNext][next]false");
}
@Test
public void sizeNonSequenceInput() throws Exception {
assertErrorContains("${coll?size}", "sequence", "evaluated to a collection");
assertOutput("${coll?sequence?size}", "[iterator][hasNext][next][hasNext][next][hasNext][next][hasNext]3");
}
@Test
public void firstTest() throws Exception {
assertOutput("${coll?first}",
"[iterator][hasNext][next]1");
assertOutput("${coll?filter(x -> x % 2 == 0)?first}",
"[iterator][hasNext][next][hasNext][next]2");
}
@Test
public void seqIndexOfTest() throws Exception {
assertOutput("${coll?seqIndexOf(2)}",
"[iterator][hasNext][next][hasNext][next]1");
assertOutput("${coll?filter(x -> x % 2 == 0)?seqIndexOf(2)}",
"[iterator][hasNext][next][hasNext][next]0");
}
@Test
public void filterTest() throws Exception {
assertOutput("${coll?filter(x -> x % 2 == 0)?filter(x -> true)?first}",
"[iterator][hasNext][next][hasNext][next]2");
}
@Test
public void listTest() throws Exception {
// Note: #list has to fetch elements up to 4 to know if it?hasNext
String commonSourceExp = "collLong?filter(x -> x % 2 == 0)";
for (String sourceExp : new String[] {commonSourceExp, "(" + commonSourceExp + ")"}) {
assertOutput("<#list " + sourceExp + " as it>${it} ${it?hasNext}<#break></#list>",
"[iterator][hasNext][next][hasNext][next][hasNext][next][hasNext][next]2 true");
}
}
@Test
public void biTargetParenthesisTest() throws Exception {
assertOutput("${(coll?filter(x -> x % 2 == 0))?first}",
"[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");
}
@Test
public void testNonDirectCalledBuiltInsAreNotLazy() throws Exception {
assertOutput("" +
"<#assign changing = 1>" +
"<#assign method = [1, 2]?filter(it -> it != changing)?join>" +
"<#assign changing = 2>" +
"${method(', ')}",
"2");
assertOutput("" +
"<#assign changing = 1>" +
"<#assign method = [1, 2]?filter(it -> it != changing)?seq_contains>" +
"<#assign changing = 2>" +
"${method(2)?c}",
"true");
assertOutput("" +
"<#assign changing = 1>" +
"<#assign method = [1, 2]?filter(it -> it != changing)?seq_index_of>" +
"<#assign changing = 2>" +
"${method(2)}",
"0");
}
public static abstract class ListContainingTemplateModel {
protected final List<Number> elements;
public ListContainingTemplateModel(Number... elements) {
this.elements = new ArrayList<>(Arrays.asList(elements));
}
public int size() {
logCall("size");
return elements.size();
}
protected void logCall(String callDesc) {
try {
Environment.getCurrentEnvironment().getOut().write("[" + callDesc + "]");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
public static class MonitoredTemplateSequenceModel extends ListContainingTemplateModel
implements TemplateSequenceModel {
public MonitoredTemplateSequenceModel(Number... elements) {
super(elements);
}
public TemplateModel get(int index) throws TemplateModelException {
logCall("get " + index);
return new SimpleNumber(elements.get(index));
}
}
public static class MonitoredTemplateCollectionModel extends ListContainingTemplateModel
implements TemplateCollectionModel {
public MonitoredTemplateCollectionModel(Number... elements) {
super(elements);
}
public TemplateModelIterator iterator() throws TemplateModelException {
logCall("iterator");
final Iterator<Number> iterator = elements.iterator();
return new TemplateModelIterator() {
public TemplateModel next() throws TemplateModelException {
logCall("next");
return new SimpleNumber(iterator.next());
}
public boolean hasNext() throws TemplateModelException {
logCall("hasNext");
return iterator.hasNext();
}
};
}
}
public static class MonitoredTemplateCollectionModelEx extends MonitoredTemplateCollectionModel
implements TemplateCollectionModelEx {
public MonitoredTemplateCollectionModelEx(Number... elements) {
super(elements);
}
public boolean isEmpty() throws TemplateModelException {
logCall("isEmpty");
return elements.isEmpty();
}
}
}