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 1053eef00b76182beebc62e1a9f289f8e2be1326 Author: ddekany <ddek...@apache.org> AuthorDate: Thu Aug 1 21:28:45 2019 +0200 Added ?take_while(predicate) and ?drop_while(predicate). --- src/main/java/freemarker/core/BuiltIn.java | 4 +- .../java/freemarker/core/BuiltInsForSequences.java | 195 +++++++++++++++++++-- src/main/java/freemarker/core/_MessageUtil.java | 6 + src/manual/en_US/book.xml | 12 +- .../java/freemarker/core/NullTransparencyTest.java | 3 +- .../core/TakeWhileAndDropWhileBiTest.java | 148 ++++++++++++++++ 6 files changed, 345 insertions(+), 23 deletions(-) diff --git a/src/main/java/freemarker/core/BuiltIn.java b/src/main/java/freemarker/core/BuiltIn.java index 5322eed..5e143b1 100644 --- a/src/main/java/freemarker/core/BuiltIn.java +++ b/src/main/java/freemarker/core/BuiltIn.java @@ -84,7 +84,7 @@ abstract class BuiltIn extends Expression implements Cloneable { static final Set<String> CAMEL_CASE_NAMES = new TreeSet<String>(); static final Set<String> SNAKE_CASE_NAMES = new TreeSet<String>(); - static final int NUMBER_OF_BIS = 281; + static final int NUMBER_OF_BIS = 285; static final HashMap<String, BuiltIn> BUILT_INS_BY_NAME = new HashMap(NUMBER_OF_BIS * 3 / 2 + 1, 1f); static { @@ -109,6 +109,7 @@ abstract class BuiltIn extends Expression implements Cloneable { putBI("datetime_if_unknown", "datetimeIfUnknown", new BuiltInsForDates.dateType_if_unknownBI(TemplateDateModel.DATETIME)); putBI("default", new BuiltInsForExistenceHandling.defaultBI()); putBI("double", new doubleBI()); + putBI("drop_while", "dropWhile", new BuiltInsForSequences.drop_whileBI()); putBI("ends_with", "endsWith", new BuiltInsForStringsBasic.ends_withBI()); putBI("ensure_ends_with", "ensureEndsWith", new BuiltInsForStringsBasic.ensure_ends_withBI()); putBI("ensure_starts_with", "ensureStartsWith", new BuiltInsForStringsBasic.ensure_starts_withBI()); @@ -278,6 +279,7 @@ abstract class BuiltIn extends Expression implements Cloneable { putBI("starts_with", "startsWith", new BuiltInsForStringsBasic.starts_withBI()); putBI("string", new BuiltInsForMultipleTypes.stringBI()); putBI("substring", new BuiltInsForStringsBasic.substringBI()); + putBI("take_while", "takeWhile", new BuiltInsForSequences.take_whileBI()); putBI("then", new BuiltInsWithLazyConditionals.then_BI()); putBI("time", new BuiltInsForMultipleTypes.dateBI(TemplateDateModel.TIME)); putBI("time_if_unknown", "timeIfUnknown", new BuiltInsForDates.dateType_if_unknownBI(TemplateDateModel.TIME)); diff --git a/src/main/java/freemarker/core/BuiltInsForSequences.java b/src/main/java/freemarker/core/BuiltInsForSequences.java index 7c8afc5..153be79 100644 --- a/src/main/java/freemarker/core/BuiltInsForSequences.java +++ b/src/main/java/freemarker/core/BuiltInsForSequences.java @@ -1000,7 +1000,27 @@ class BuiltInsForSequences { } - static class filterBI extends IntermediateStreamOperationLikeBuiltIn { + private static abstract class FilterLikeBI extends IntermediateStreamOperationLikeBuiltIn { + protected final boolean elementMatches(TemplateModel element, ElementTransformer elementTransformer, + Environment env) + throws TemplateException { + TemplateModel transformedElement = elementTransformer.transformElement(element, env); + if (!(transformedElement instanceof TemplateBooleanModel)) { + if (transformedElement == null) { + throw new _TemplateModelException(getElementTransformerExp(), env, + "The filter expression has returned no value (has returned null), " + + "rather than a boolean."); + } + throw new _TemplateModelException(getElementTransformerExp(), env, + "The filter expression had to return a boolean value, but it returned ", + new _DelayedAOrAn(new _DelayedFTLTypeDescription(transformedElement)), + " instead."); + } + return ((TemplateBooleanModel) transformedElement).getAsBoolean(); + } + } + + static class filterBI extends FilterLikeBI { protected TemplateModel calculateResult( final TemplateModelIterator lhoIterator, final TemplateModel lho, @@ -1073,21 +1093,79 @@ class BuiltInsForSequences { } } - private boolean elementMatches(TemplateModel element, ElementTransformer elementTransformer, Environment env) - throws TemplateException { - TemplateModel transformedElement = elementTransformer.transformElement(element, env); - if (!(transformedElement instanceof TemplateBooleanModel)) { - if (transformedElement == null) { - throw new _TemplateModelException(getElementTransformerExp(), env, - "The filter expression has returned no value (has returned null), " + - "rather than a boolean."); + } + + static class take_whileBI extends FilterLikeBI { + + protected TemplateModel calculateResult( + final TemplateModelIterator lhoIterator, final TemplateModel lho, + boolean lhoIsSequence, final ElementTransformer elementTransformer, + final Environment env) throws TemplateException { + if (!isLazilyGeneratedResultEnabled()) { + if (!lhoIsSequence) { + throw _MessageUtil.newLazilyGeneratedCollectionMustBeSequenceException(take_whileBI.this); } - throw new _TemplateModelException(getElementTransformerExp(), env, - "The filter expression had to return a boolean value, but it returned ", - new _DelayedAOrAn(new _DelayedFTLTypeDescription(transformedElement)), - " instead."); + + List<TemplateModel> resultList = new ArrayList<TemplateModel>(); + while (lhoIterator.hasNext()) { + TemplateModel element = lhoIterator.next(); + if (elementMatches(element, elementTransformer, env)) { + resultList.add(element); + } else { + break; + } + } + return new TemplateModelListSequence(resultList); + } else { + return new LazilyGeneratedCollectionModelWithUnknownSize( + new TemplateModelIterator() { + boolean prefetchDone; + TemplateModel prefetchedElement; + boolean prefetchedEndOfIterator; + + public TemplateModel next() throws TemplateModelException { + ensurePrefetchDone(); + if (prefetchedEndOfIterator) { + throw new IllegalStateException("next() was called when hasNext() is false"); + } + prefetchDone = false; + return prefetchedElement; + } + + public boolean hasNext() throws TemplateModelException { + ensurePrefetchDone(); + return !prefetchedEndOfIterator; + } + + private void ensurePrefetchDone() throws TemplateModelException { + if (prefetchDone) { + return; + } + + if (lhoIterator.hasNext()) { + TemplateModel element = lhoIterator.next(); + boolean elementMatched; + try { + elementMatched = elementMatches(element, elementTransformer, env); + } catch (TemplateException e) { + throw new _TemplateModelException(e, env, "Failed to transform element"); + } + if (elementMatched) { + prefetchedElement = element; + } else { + prefetchedEndOfIterator = true; + prefetchedElement = null; + } + } else { + prefetchedEndOfIterator = true; + prefetchedElement = null; + } + prefetchDone = true; + } + }, + lhoIsSequence + ); } - return ((TemplateBooleanModel) transformedElement).getAsBoolean(); } } @@ -1147,6 +1225,95 @@ class BuiltInsForSequences { } + static class drop_whileBI extends FilterLikeBI { + + protected TemplateModel calculateResult( + final TemplateModelIterator lhoIterator, final TemplateModel lho, + boolean lhoIsSequence, final ElementTransformer elementTransformer, + final Environment env) throws TemplateException { + if (!isLazilyGeneratedResultEnabled()) { + if (!lhoIsSequence) { + throw _MessageUtil.newLazilyGeneratedCollectionMustBeSequenceException(drop_whileBI.this); + } + + List<TemplateModel> resultList = new ArrayList<TemplateModel>(); + while (lhoIterator.hasNext()) { + TemplateModel element = lhoIterator.next(); + if (!elementMatches(element, elementTransformer, env)) { + resultList.add(element); + while (lhoIterator.hasNext()) { + resultList.add(lhoIterator.next()); + } + break; + } + } + return new TemplateModelListSequence(resultList); + } else { + return new LazilyGeneratedCollectionModelWithUnknownSize( + new TemplateModelIterator() { + boolean dropMode = true; + boolean prefetchDone; + TemplateModel prefetchedElement; + boolean prefetchedEndOfIterator; + + public TemplateModel next() throws TemplateModelException { + ensurePrefetchDone(); + if (prefetchedEndOfIterator) { + throw new IllegalStateException("next() was called when hasNext() is false"); + } + prefetchDone = false; + return prefetchedElement; + } + + public boolean hasNext() throws TemplateModelException { + ensurePrefetchDone(); + return !prefetchedEndOfIterator; + } + + private void ensurePrefetchDone() throws TemplateModelException { + if (prefetchDone) { + return; + } + + if (dropMode) { + boolean foundElement = false; + dropElements: while (lhoIterator.hasNext()) { + TemplateModel element = lhoIterator.next(); + try { + if (!elementMatches(element, elementTransformer, env)) { + prefetchedElement = element; + foundElement = true; + break dropElements; + } + } catch (TemplateException e) { + throw new _TemplateModelException(e, env, + "Failed to transform element"); + } + } + dropMode = false; + if (!foundElement) { + prefetchedEndOfIterator = true; + prefetchedElement = null; + } + } else { + if (lhoIterator.hasNext()) { + TemplateModel element = lhoIterator.next(); + prefetchedElement = element; + } else { + prefetchedEndOfIterator = true; + prefetchedElement = null; + } + } + prefetchDone = true; + } + }, + lhoIsSequence + ); + } + } + + } + // Can't be instantiated private BuiltInsForSequences() { } diff --git a/src/main/java/freemarker/core/_MessageUtil.java b/src/main/java/freemarker/core/_MessageUtil.java index 4d2df6a..ebbac10 100644 --- a/src/main/java/freemarker/core/_MessageUtil.java +++ b/src/main/java/freemarker/core/_MessageUtil.java @@ -342,6 +342,12 @@ public class _MessageUtil { ", which leads to this restriction.")); } + /** + * Because of the limitations of FTL lambdas (called "local lambdas"), sometimes we must condense the lazy result + * down into a sequence. However, doing that automatically is only allowed if the input was a sequence as well. If + * it wasn't a sequence, we don't dare to collect the result into a sequence automatically (because it's possibly + * too long), and that's when this error message comes. + */ public static TemplateException newLazilyGeneratedCollectionMustBeSequenceException(Expression blamed) { return new _MiscTemplateException(blamed, "The result is a listable value with lazy transformation(s) applied on it, but it's not " + diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml index 931f768..ec49d8e 100644 --- a/src/manual/en_US/book.xml +++ b/src/manual/en_US/book.xml @@ -20,11 +20,7 @@ <book conformance="docgen" version="5.0" xml:lang="en" xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" - xmlns:xi="http://www.w3.org/2001/XInclude" - xmlns:ns5="http://www.w3.org/1999/xhtml" - xmlns:ns4="http://www.w3.org/2000/svg" - xmlns:ns3="http://www.w3.org/1998/Math/MathML" - xmlns:ns="http://docbook.org/ns/docbook"> +> <info> <title>Apache FreeMarker Manual</title> @@ -27900,8 +27896,10 @@ TemplateModel x = env.getVariable("x"); // get variable x</programlisting> <itemizedlist> <listitem> <para>Added new built-ins: - <literal>?filter(<replaceable>predicate</replaceable>)</literal> - and <literal>?map(<replaceable>mapper</replaceable>)</literal>. + <literal>?filter(<replaceable>predicate</replaceable>)</literal>, + <literal>?map(<replaceable>mapper</replaceable>)</literal>, + <literal>?take_while(<replaceable>predicate</replaceable>)</literal>, + <literal>?drop_while(<replaceable>predicate</replaceable>)</literal>. These allow using lambda expression, like <literal>users?filter(user -> user.superuser)</literal> or <literal>users?map(user -> user.name)</literal>, or accept a diff --git a/src/test/java/freemarker/core/NullTransparencyTest.java b/src/test/java/freemarker/core/NullTransparencyTest.java index bde9957..a5f6638 100644 --- a/src/test/java/freemarker/core/NullTransparencyTest.java +++ b/src/test/java/freemarker/core/NullTransparencyTest.java @@ -56,7 +56,6 @@ public class NullTransparencyTest extends TemplateTest { @Test public void testWithoutClashingHigherScopeVar() throws Exception { - assertTrue(getConfiguration().getFallbackOnNullLoopVariable()); testLambdaArguments(); testLoopVariables("null"); @@ -83,6 +82,8 @@ public class NullTransparencyTest extends TemplateTest { protected void testLambdaArguments() throws IOException, TemplateException { assertOutput("<#list list?filter(it -> it??) as it>${it!'null'}<#sep>, </#list>", "a, b"); + assertOutput("<#list list?takeWhile(it -> it??) as it>${it!'null'}<#sep>, </#list>", + "a"); assertOutput("<#list list?map(it -> it!'null') as it>${it}<#sep>, </#list>", "a, null, b"); } diff --git a/src/test/java/freemarker/core/TakeWhileAndDropWhileBiTest.java b/src/test/java/freemarker/core/TakeWhileAndDropWhileBiTest.java new file mode 100644 index 0000000..b55e85d --- /dev/null +++ b/src/test/java/freemarker/core/TakeWhileAndDropWhileBiTest.java @@ -0,0 +1,148 @@ +/* + * 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.util.Collections; +import java.util.List; + +import org.junit.Test; + +import com.google.common.collect.ImmutableList; + +import freemarker.template.Configuration; +import freemarker.template.DefaultObjectWrapper; +import freemarker.test.TemplateTest; + +public class TakeWhileAndDropWhileBiTest extends TemplateTest { + + private static class TestParam { + private final List<?> list; + private final String takeWhileResult; + private final String dropWhileResult; + + public TestParam(List<?> list, String takeWhileResult, String dropWhileResult) { + this.list = list; + this.takeWhileResult = takeWhileResult; + this.dropWhileResult = dropWhileResult; + } + } + + @Override + protected Configuration createConfiguration() throws Exception { + Configuration cfg = super.createConfiguration(); + + DefaultObjectWrapper objectWrapper = new DefaultObjectWrapper(Configuration.VERSION_2_3_28); + objectWrapper.setForceLegacyNonListCollections(false); + cfg.setObjectWrapper(objectWrapper); + + return cfg; + } + + private static final List<TestParam> TEST_PARAMS = ImmutableList.of( + new TestParam(ImmutableList.of(), + "", + ""), + new TestParam(ImmutableList.of("a"), + "a", + "a"), + new TestParam(ImmutableList.of("a", "b", "c"), + "a, b, c", + "a, b, c"), + new TestParam(ImmutableList.of("aX"), + "", + ""), + new TestParam(ImmutableList.of("aX", "b"), + "", + "b"), + new TestParam(ImmutableList.of("aX", "b", "c"), + "", + "b, c"), + new TestParam(ImmutableList.of("a", "bX", "c"), + "a", + "a, bX, c"), + new TestParam(ImmutableList.of("a", "b", "cX"), + "a, b", + "a, b, cX"), + new TestParam(ImmutableList.of("aX", "bX", "c"), + "", + "c"), + new TestParam(ImmutableList.of("aX", "bX", "cX"), + "", + ""), + new TestParam(ImmutableList.of("aX", "b", "cX"), + "", + "b, cX") + ); + + @Test + public void testTakeWhile() throws Exception { + for (TestParam testParam : TEST_PARAMS) { + addToDataModel("xs", testParam.list); + assertOutput( + "<#list xs?takeWhile(it -> !it?contains('X')) as x>${x}<#sep>, </#list>", + testParam.takeWhileResult); + assertOutput( + "<#assign fxs = xs?takeWhile(it -> !it?contains('X'))>" + + "${fxs?join(', ')}", + testParam.takeWhileResult); + } + } + + @Test + public void testDropWhile() throws Exception { + for (TestParam testParam : TEST_PARAMS) { + addToDataModel("xs", testParam.list); + assertOutput( + "<#list xs?dropWhile(it -> it?contains('X')) as x>${x}<#sep>, </#list>", + testParam.dropWhileResult); + assertOutput( + "<#assign fxs = xs?dropWhile(it -> it?contains('X'))>" + + "${fxs?join(', ')}", + testParam.dropWhileResult); + } + } + + // Chaining the two built-ins is not a special case, but, in the hope of running into some bugs, we test that too. + @Test + public void testBetween() throws Exception { + String ftl = "<#list xs?dropWhile(it -> it < 0)?takeWhile(it -> it >= 0) as x>${x}<#sep>, </#list>"; + + addToDataModel("xs", ImmutableList.of(-1, -2, 3, 4, -5, -6)); + assertOutput(ftl, "3, 4"); + + addToDataModel("xs", ImmutableList.of(-1, -2, -5, -6)); + assertOutput(ftl, ""); + + addToDataModel("xs", ImmutableList.of(1, 2, 3)); + assertOutput(ftl, "1, 2, 3"); + + addToDataModel("xs", Collections.emptyList()); + assertOutput(ftl, ""); + } + + @Test + public void testSnakeCaseNames() throws Exception { + addToDataModel("xs", ImmutableList.of(-1, -2, 3, 4, -5, -6)); + assertOutput( + "<#list xs?drop_while(it -> it < 0)?take_while(it -> it >= 0) as x>${x}<#sep>, </#list>", + "3, 4"); + } + +} \ No newline at end of file