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/08/01 19:56:34 UTC
[freemarker] 02/03: Added ?take_while(predicate) and
?drop_while(predicate).
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 <dd...@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