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 abaed15caf0d3e43da2d1e6abfecbda7b03c20f9 Author: ddekany <[email protected]> AuthorDate: Sun Mar 31 16:43:31 2019 +0200 Some cleanup related to local lambdas, ?filter and ?map. Added optimizations to ?size operating on the result of ?filter/?map. --- src/main/java/freemarker/core/BuiltIn.java | 6 +- .../freemarker/core/BuiltInsForMultipleTypes.java | 21 +++ .../java/freemarker/core/BuiltInsForSequences.java | 101 +++++++----- src/main/java/freemarker/core/Environment.java | 2 - src/main/java/freemarker/core/IteratorBlock.java | 4 +- .../core/LazilyGeneratedSequenceModel.java | 35 +++++ .../core/LazyCollectionTemplateModelIterator.java | 54 +++++++ .../java/freemarker/core/LazySequenceIterator.java | 55 +++++++ .../core/SameSizeLazilyGeneratedSequenceModel.java | 49 ++++++ .../SameSizeSeqLazilyGeneratedSequenceModel.java | 52 ++++++ ...ilyGeneratedSeqTargetSupportInBuiltinsTest.java | 174 +++++++++++++++++++++ .../core/ListWithStreamLikeBuiltinsTest.java | 2 +- 12 files changed, 507 insertions(+), 48 deletions(-) diff --git a/src/main/java/freemarker/core/BuiltIn.java b/src/main/java/freemarker/core/BuiltIn.java index 450dd6d..0679e21 100644 --- a/src/main/java/freemarker/core/BuiltIn.java +++ b/src/main/java/freemarker/core/BuiltIn.java @@ -387,16 +387,16 @@ abstract class BuiltIn extends Expression implements Cloneable { } bi.key = key; bi.target = target; - if (bi.isSingleIterationCollectionTargetSupported()) { + if (bi.isLazilyGeneratedSequenceModelTargetSupported()) { if (target instanceof BuiltInsForSequences.IntermediateStreamOperationLikeBuiltIn) { ((BuiltInsForSequences.IntermediateStreamOperationLikeBuiltIn) target) - .setLazyProcessingAllowed(true); + .setLazyResultGenerationAllowed(true); } } return bi; } - protected boolean isSingleIterationCollectionTargetSupported() { + protected boolean isLazilyGeneratedSequenceModelTargetSupported() { return false; } diff --git a/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java b/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java index e0640c2..61f8f3f 100644 --- a/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java +++ b/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java @@ -40,6 +40,7 @@ import freemarker.template.TemplateHashModelEx; import freemarker.template.TemplateMethodModel; import freemarker.template.TemplateModel; import freemarker.template.TemplateModelException; +import freemarker.template.TemplateModelIterator; import freemarker.template.TemplateModelWithAPISupport; import freemarker.template.TemplateNodeModel; import freemarker.template.TemplateNumberModel; @@ -481,6 +482,12 @@ class BuiltInsForMultipleTypes { } static class sizeBI extends BuiltIn { + + @Override + protected boolean isLazilyGeneratedSequenceModelTargetSupported() { + return true; + } + @Override TemplateModel _eval(Environment env) throws TemplateException { TemplateModel model = target.eval(env); @@ -492,6 +499,20 @@ class BuiltInsForMultipleTypes { size = ((TemplateCollectionModelEx) model).size(); } else if (model instanceof TemplateHashModelEx) { size = ((TemplateHashModelEx) model).size(); + } else if (model instanceof LazilyGeneratedSequenceModel) { + // While this is a TemplateCollectionModel, and thus ?size will be O(N), and N might be infinite, + // it's for the result of ?filter(predicate) or such. Those "officially" return a sequence. Returning a + // TemplateCollectionModel (a LazilyGeneratedSequenceModel more specifically) is a (mostly) transparent + // optimization to avoid creating the result sequence in memory, which would be unnecessary work for + // ?size. Creating that result sequence would be O(N) too, so the O(N) time complexity should be + // expected by the template author, and we just made that calculation less wasteful here. + TemplateModelIterator iterator = ((LazilyGeneratedSequenceModel) model).iterator(); + int counter = 0; + while (iterator.hasNext()) { + counter++; + iterator.next(); + } + size = counter; } else { throw new UnexpectedTypeException( target, model, diff --git a/src/main/java/freemarker/core/BuiltInsForSequences.java b/src/main/java/freemarker/core/BuiltInsForSequences.java index 0bb2bd9..b4acd16 100644 --- a/src/main/java/freemarker/core/BuiltInsForSequences.java +++ b/src/main/java/freemarker/core/BuiltInsForSequences.java @@ -144,7 +144,7 @@ class BuiltInsForSequences { static class firstBI extends BuiltIn { @Override - protected boolean isSingleIterationCollectionTargetSupported() { + protected boolean isLazilyGeneratedSequenceModelTargetSupported() { return true; } @@ -185,7 +185,7 @@ class BuiltInsForSequences { static class joinBI extends BuiltIn { @Override - protected boolean isSingleIterationCollectionTargetSupported() { + protected boolean isLazilyGeneratedSequenceModelTargetSupported() { return true; } @@ -302,7 +302,7 @@ class BuiltInsForSequences { static class seq_containsBI extends BuiltIn { @Override - protected boolean isSingleIterationCollectionTargetSupported() { + protected boolean isLazilyGeneratedSequenceModelTargetSupported() { return true; } @@ -374,7 +374,7 @@ class BuiltInsForSequences { static class seq_index_ofBI extends BuiltIn { @Override - protected boolean isSingleIterationCollectionTargetSupported() { + protected boolean isLazilyGeneratedSequenceModelTargetSupported() { return true; } @@ -915,7 +915,7 @@ class BuiltInsForSequences { } @Override - protected boolean isSingleIterationCollectionTargetSupported() { + protected boolean isLazilyGeneratedSequenceModelTargetSupported() { return true; } @@ -980,19 +980,20 @@ class BuiltInsForSequences { } /** - * Built-in that's similar to an Java 8 Stream intermediate operation. To be on the safe side, by default these - * are eager, and just produce a {@link TemplateSequenceModel}. But when circumstances allow, they become - * lazy, similarly to Java 8 Stream-s. Another characteristic of the built-ins that they usually accept - * lambda expressions as parameters. + * Built-in that's similar to a Java 8 Stream intermediate operation. To be on the safe side, by default these + * are eager, and just produce a {@link TemplateSequenceModel}. But when circumstances allow, they become lazy, + * similarly to Java 8 Stream intermediate operations. Another characteristic of these built-ins is that they + * usually accept lambda expressions as parameters. */ static abstract class IntermediateStreamOperationLikeBuiltIn extends BuiltInWithParseTimeParameters { private Expression elementTransformerExp; private ElementTransformer precreatedElementTransformer; - private boolean lazyProcessingAllowed; + private boolean lazyResultGenerationAllowed; @Override void bindToParameters(List<Expression> parameters, Token openParen, Token closeParen) throws ParseException { + // At the moment all built-ins of this kind requires 1 parameter. if (parameters.size() != 1) { throw newArgumentCountException("requires exactly 1", openParen, closeParen); } @@ -1003,10 +1004,6 @@ class BuiltInsForSequences { // We can't do this with other kind of expressions, as they need to be evaluated on runtime: precreatedElementTransformer = new LocalLambdaElementTransformer(localLambdaExp); } - - if (target instanceof IntermediateStreamOperationLikeBuiltIn) { - ((IntermediateStreamOperationLikeBuiltIn) target).setLazyProcessingAllowed(true); - } } @Override @@ -1014,12 +1011,17 @@ class BuiltInsForSequences { return true; } - boolean isLazyProcessingAllowed() { - return lazyProcessingAllowed; + @Override + protected boolean isLazilyGeneratedSequenceModelTargetSupported() { + return true; + } + + final boolean isLazyResultGenerationAllowed() { + return lazyResultGenerationAllowed; } /** - * Used to allow processing of the collection or sequence elements on an as-needed basis, similarly as + * Used to allow generating the result collection or sequence elements on an as-needed basis, similarly as * Java 8 Stream intermediate operations do it. This is initially {@code false}. The containing expression or * directive sets it to {@code true} if it can ensure that: * <ul> @@ -1030,8 +1032,8 @@ class BuiltInsForSequences { * built-in was called. This is required as lambda expression are {@link LocalLambdaExpression}-s. * </ul> */ - void setLazyProcessingAllowed(boolean lazyProcessingAllowed) { - this.lazyProcessingAllowed = lazyProcessingAllowed; + void setLazyResultGenerationAllowed(boolean lazyResultGenerationAllowed) { + this.lazyResultGenerationAllowed = lazyResultGenerationAllowed; } protected List<Expression> getArgumentsAsList() { @@ -1084,10 +1086,12 @@ class BuiltInsForSequences { private TemplateModelIterator getTemplateModelIterator(Environment env, TemplateModel model) throws TemplateModelException, NonSequenceOrCollectionException, InvalidReferenceException { if (model instanceof TemplateCollectionModel) { - return ((TemplateCollectionModel) model).iterator(); + return isLazyResultGenerationAllowed() + ? new LazyCollectionTemplateModelIterator((TemplateCollectionModel) model) + : ((TemplateCollectionModel) model).iterator(); } else if (model instanceof TemplateSequenceModel) { - return new SequenceIterator((TemplateSequenceModel) model); - } else if (model instanceof TemplateModelIterator) { // For a stream mode LHO + return new LazySequenceIterator((TemplateSequenceModel) model); + } else if (model instanceof TemplateModelIterator) { // For a lazily generated LHO return (TemplateModelIterator) model; } else { throw new NonSequenceOrCollectionException(target, model, env); @@ -1095,8 +1099,10 @@ class BuiltInsForSequences { } /** - * @param lhoIterator Use this to iterate through the items - * @param lho Maybe needed for operations specific to the built-in, like getting the size + * @param lhoIterator Use this to read the elements of the left hand operand + * @param lho Maybe needed for operations specific to the built-in, like getting the size, otherwise use the + * {@code lhoIterator} only. + * @param elementTransformer The argument to the built-in (typically a lambda expression) * * @return {@link TemplateSequenceModel} or {@link TemplateCollectionModel} or {@link TemplateModelIterator}. */ @@ -1104,10 +1110,15 @@ class BuiltInsForSequences { TemplateModelIterator lhoIterator, TemplateModel lho, ElementTransformer elementTransformer, Environment env) throws TemplateException; + /** + * Wraps the built-in argument that specifies how to transform the elements of the sequence, to hide the + * complexity of doing that. + */ interface ElementTransformer { TemplateModel transformElement(TemplateModel element, Environment env) throws TemplateException; } + /** {@link ElementTransformer} that wraps a local lambda expression. */ private static class LocalLambdaElementTransformer implements ElementTransformer { private final LocalLambdaExpression elementTransformerExp; @@ -1120,6 +1131,7 @@ class BuiltInsForSequences { } } + /** {@link ElementTransformer} that wraps a (Java) method call. */ private static class MethodElementTransformer implements ElementTransformer { private final TemplateMethodModel elementTransformer; @@ -1134,6 +1146,7 @@ class BuiltInsForSequences { } } + /** {@link ElementTransformer} that wraps a call to an FTL function (things defined with {@code #function}). */ private static class FunctionElementTransformer implements ElementTransformer { private final Macro templateTransformer; private final Expression elementTransformerExp; @@ -1145,6 +1158,8 @@ class BuiltInsForSequences { public TemplateModel transformElement(TemplateModel element, Environment env) throws TemplateException { + // #function-s were originally designed to be called from templates directly, so they expect an + // Expression as argument. So we have to create a fake one. ExpressionWithFixedResult functionArgExp = new ExpressionWithFixedResult( element, elementTransformerExp); return env.invokeFunction(env, templateTransformer, @@ -1161,7 +1176,7 @@ class BuiltInsForSequences { final TemplateModelIterator lhoIterator, final TemplateModel lho, final ElementTransformer elementTransformer, final Environment env) throws TemplateException { - if (!isLazyProcessingAllowed()) { + if (!isLazyResultGenerationAllowed()) { List<TemplateModel> resultList = new ArrayList<TemplateModel>(); while (lhoIterator.hasNext()) { TemplateModel element = lhoIterator.next(); @@ -1171,7 +1186,7 @@ class BuiltInsForSequences { } return new TemplateModelListSequence(resultList); } else { - return new SingleIterationCollectionModel( + return new LazilyGeneratedSequenceModel( new TemplateModelIterator() { boolean prefetchDone; TemplateModel prefetchedElement; @@ -1223,17 +1238,17 @@ class BuiltInsForSequences { } } - private boolean elementMatches(TemplateModel element, ElementTransformer elementTransformer, Environment env) throws - TemplateException { + 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 element transformer function has returned no return value (has returned null) " + - "instead of a boolean."); + "The filter expression has returned no value (has returned null), " + + "rather than a boolean."); } throw new _TemplateModelException(getElementTransformerExp(), env, - "The element transformer function had to return a boolean value, but it has returned ", + "The filter expression had to return a boolean value, but it returned ", new _DelayedAOrAn(new _DelayedFTLTypeDescription(transformedElement)), " instead."); } @@ -1247,19 +1262,17 @@ class BuiltInsForSequences { protected TemplateModel calculateResult( final TemplateModelIterator lhoIterator, TemplateModel lho, final ElementTransformer elementTransformer, final Environment env) throws TemplateException { - if (!isLazyProcessingAllowed()) { + if (!isLazyResultGenerationAllowed()) { List<TemplateModel> resultList = new ArrayList<TemplateModel>(); while (lhoIterator.hasNext()) { - resultList.add(fetchAndTransformNextElement(lhoIterator, elementTransformer, env)); + resultList.add(fetchAndMapNextElement(lhoIterator, elementTransformer, env)); } return new TemplateModelListSequence(resultList); } else { - return new SingleIterationCollectionModel( - new TemplateModelIterator() { - + TemplateModelIterator mappedLhoIterator = new TemplateModelIterator() { public TemplateModel next() throws TemplateModelException { try { - return fetchAndTransformNextElement(lhoIterator, elementTransformer, env); + return fetchAndMapNextElement(lhoIterator, elementTransformer, env); } catch (TemplateException e) { throw new _TemplateModelException(e, env, "Failed to transform element"); } @@ -1268,17 +1281,25 @@ class BuiltInsForSequences { public boolean hasNext() throws TemplateModelException { return lhoIterator.hasNext(); } - }); + }; + if (lho instanceof TemplateCollectionModelEx) { // Preferred branch, as TempCollModEx has isEmpty() too + return new SameSizeCollLazilyGeneratedSequenceModel(mappedLhoIterator, + (TemplateCollectionModelEx) lho); + } else if (lho instanceof TemplateSequenceModel) { + return new SameSizeSeqLazilyGeneratedSequenceModel(mappedLhoIterator, (TemplateSequenceModel) lho); + } else { + return new LazilyGeneratedSequenceModel(mappedLhoIterator); + } } } - private TemplateModel fetchAndTransformNextElement( + private TemplateModel fetchAndMapNextElement( TemplateModelIterator lhoIterator, ElementTransformer elementTransformer, Environment env) throws TemplateException { TemplateModel transformedElement = elementTransformer.transformElement(lhoIterator.next(), env); if (transformedElement == null) { throw new _TemplateModelException(getElementTransformerExp(), env, - "The element transformer function has returned no return value (has returned null)."); + "The element mapper function has returned no return value (has returned null)."); } return transformedElement; } diff --git a/src/main/java/freemarker/core/Environment.java b/src/main/java/freemarker/core/Environment.java index eff4c91..2bacc33 100644 --- a/src/main/java/freemarker/core/Environment.java +++ b/src/main/java/freemarker/core/Environment.java @@ -684,8 +684,6 @@ public final class Environment extends Configurable { /** * Evaluate expression with shadowing a single variable with a new local variable. - * - * @since 2.3.29 */ TemplateModel evaluateWithNewLocal(Expression exp, String lambdaArgName, TemplateModel lamdaArgValue) throws TemplateException { diff --git a/src/main/java/freemarker/core/IteratorBlock.java b/src/main/java/freemarker/core/IteratorBlock.java index 59e3c35..a51401a 100644 --- a/src/main/java/freemarker/core/IteratorBlock.java +++ b/src/main/java/freemarker/core/IteratorBlock.java @@ -84,7 +84,7 @@ final class IteratorBlock extends TemplateElement { this.forEach = forEach; if (listedExp instanceof BuiltInsForSequences.IntermediateStreamOperationLikeBuiltIn) { - ((BuiltInsForSequences.IntermediateStreamOperationLikeBuiltIn) listedExp).setLazyProcessingAllowed(true); + ((BuiltInsForSequences.IntermediateStreamOperationLikeBuiltIn) listedExp).setLazyResultGenerationAllowed(true); fetchElementsOutsideLoopVarContext = true; } else { fetchElementsOutsideLoopVarContext = false; @@ -307,7 +307,7 @@ final class IteratorBlock extends TemplateElement { openedIterator = null; } else { // We must reuse this later, because TemplateCollectionModel-s that wrap an Iterator only - // allow one iterator() call. (Also those returned by ?filter, etc. with lazy processing on.) + // allow one iterator() call. (Also those returned by ?filter, etc. with lazy result generation.) openedIterator = iterModel; // Note: Loop variables will only become visible inside #items env.visit(childBuffer); diff --git a/src/main/java/freemarker/core/LazilyGeneratedSequenceModel.java b/src/main/java/freemarker/core/LazilyGeneratedSequenceModel.java new file mode 100644 index 0000000..a0737f1 --- /dev/null +++ b/src/main/java/freemarker/core/LazilyGeneratedSequenceModel.java @@ -0,0 +1,35 @@ +/* + * 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.ext.beans.CollectionModel; +import freemarker.template.TemplateModelIterator; +import freemarker.template.TemplateSequenceModel; + +/** + * Same as {@link SingleIterationCollectionModel}, but marks the value as something that's in principle a + * {@link TemplateSequenceModel}, but to allow lazy result generation a {@link CollectionModel} is used internally. + * This is an optimization that we do where we consider it to be transparent enough for the user. + */ +class LazilyGeneratedSequenceModel extends SingleIterationCollectionModel { + public LazilyGeneratedSequenceModel(TemplateModelIterator iterator) { + super(iterator); + } +} diff --git a/src/main/java/freemarker/core/LazyCollectionTemplateModelIterator.java b/src/main/java/freemarker/core/LazyCollectionTemplateModelIterator.java new file mode 100644 index 0000000..aa55c26 --- /dev/null +++ b/src/main/java/freemarker/core/LazyCollectionTemplateModelIterator.java @@ -0,0 +1,54 @@ +/* + * 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.TemplateCollectionModel; +import freemarker.template.TemplateModel; +import freemarker.template.TemplateModelException; +import freemarker.template.TemplateModelIterator; + +/** + * Delays creating the {@link TemplateModelIterator} of the wrapped model until it's actually needed. + */ +class LazyCollectionTemplateModelIterator implements TemplateModelIterator { + + private final TemplateCollectionModel templateCollectionModel; + private TemplateModelIterator iterator; + + public LazyCollectionTemplateModelIterator(TemplateCollectionModel templateCollectionModel) { + this.templateCollectionModel = templateCollectionModel; + } + + public TemplateModel next() throws TemplateModelException { + ensureIteratorInitialized(); + return iterator.next(); + } + + public boolean hasNext() throws TemplateModelException { + ensureIteratorInitialized(); + return iterator.hasNext(); + } + + private void ensureIteratorInitialized() throws TemplateModelException { + if (iterator == null) { + iterator = templateCollectionModel.iterator(); + } + } +} diff --git a/src/main/java/freemarker/core/LazySequenceIterator.java b/src/main/java/freemarker/core/LazySequenceIterator.java new file mode 100644 index 0000000..8bcf78d --- /dev/null +++ b/src/main/java/freemarker/core/LazySequenceIterator.java @@ -0,0 +1,55 @@ +/* + * 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.TemplateModel; +import freemarker.template.TemplateModelException; +import freemarker.template.TemplateModelIterator; +import freemarker.template.TemplateSequenceModel; + +/** + * {@link TemplateModelIterator} that wraps a sequence, delaying calling any of its methods until it's actually needed. + * + * @since 2.3.29 + */ +class LazySequenceIterator implements TemplateModelIterator { + private final TemplateSequenceModel sequence; + private Integer size; + private int index = 0; + + LazySequenceIterator(TemplateSequenceModel sequence) throws TemplateModelException { + this.sequence = sequence; + + } + public TemplateModel next() throws TemplateModelException { + return sequence.get(index++); + } + + public boolean hasNext() { + if (size == null) { + try { + size = sequence.size(); + } catch (TemplateModelException e) { + throw new RuntimeException("Error when getting sequence size", e); + } + } + return index < size; + } +} diff --git a/src/main/java/freemarker/core/SameSizeLazilyGeneratedSequenceModel.java b/src/main/java/freemarker/core/SameSizeLazilyGeneratedSequenceModel.java new file mode 100644 index 0000000..a0f4225 --- /dev/null +++ b/src/main/java/freemarker/core/SameSizeLazilyGeneratedSequenceModel.java @@ -0,0 +1,49 @@ +/* + * 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; +import freemarker.template.utility.NullArgumentException; + +/** + * Used instead of {@link LazilyGeneratedSequenceModel} for operations that don't change the element count of the + * source, if the source can also give back an element count. + */ +class SameSizeCollLazilyGeneratedSequenceModel extends LazilyGeneratedSequenceModel + implements TemplateCollectionModelEx { + private final TemplateCollectionModelEx sizeSourceCollEx; + + public SameSizeCollLazilyGeneratedSequenceModel( + TemplateModelIterator iterator, TemplateCollectionModelEx sizeSourceCollEx) { + super(iterator); + NullArgumentException.check(sizeSourceCollEx); + this.sizeSourceCollEx = sizeSourceCollEx; + } + + public int size() throws TemplateModelException { + return sizeSourceCollEx.size(); + } + + public boolean isEmpty() throws TemplateModelException { + return sizeSourceCollEx.isEmpty(); + } +} diff --git a/src/main/java/freemarker/core/SameSizeSeqLazilyGeneratedSequenceModel.java b/src/main/java/freemarker/core/SameSizeSeqLazilyGeneratedSequenceModel.java new file mode 100644 index 0000000..d6d7b5f --- /dev/null +++ b/src/main/java/freemarker/core/SameSizeSeqLazilyGeneratedSequenceModel.java @@ -0,0 +1,52 @@ +/* + * 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; +import freemarker.template.TemplateSequenceModel; +import freemarker.template.utility.NullArgumentException; + +/** + * Used instead of {@link LazilyGeneratedSequenceModel} for operations that don't change the element count of the + * source, if the source can also give back an element count. + * + * @since 2.3.29 + */ +class SameSizeSeqLazilyGeneratedSequenceModel extends LazilyGeneratedSequenceModel + implements TemplateCollectionModelEx { + private final TemplateSequenceModel sizeSourceSeq; + + public SameSizeSeqLazilyGeneratedSequenceModel( + TemplateModelIterator iterator, TemplateSequenceModel sizeSourceSeq) { + super(iterator); + NullArgumentException.check(sizeSourceSeq); + this.sizeSourceSeq = sizeSourceSeq; + } + + public int size() throws TemplateModelException { + return sizeSourceSeq.size(); + } + + public boolean isEmpty() throws TemplateModelException { + return sizeSourceSeq.size() == 0; + } +} diff --git a/src/test/java/freemarker/core/LazilyGeneratedSeqTargetSupportInBuiltinsTest.java b/src/test/java/freemarker/core/LazilyGeneratedSeqTargetSupportInBuiltinsTest.java new file mode 100644 index 0000000..37cefa8 --- /dev/null +++ b/src/test/java/freemarker/core/LazilyGeneratedSeqTargetSupportInBuiltinsTest.java @@ -0,0 +1,174 @@ +/* + * 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.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 built-ins that are support getting {@link LazilyGeneratedSequenceModel} as their LHO input. + */ +public class LazilyGeneratedSeqTargetSupportInBuiltinsTest extends TemplateTest { + + @Test + public void sizeTest() 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?map(x -> x * 10)?size}", + "[size]3"); + + assertOutput("${seq?filter(x -> x != 1)?size}", + "[size][get 0][get 1][get 2]2"); + assertOutput("${collEx?filter(x -> x != 1)?size}", + "[iterator][hasNext][next][hasNext][next][hasNext][next][hasNext]2"); + } + + @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 + assertOutput("<#list collLong?filter(x -> x % 2 == 0) as it>${it} ${it?hasNext?c}<#break></#list>", + "[iterator][hasNext][next][hasNext][next][hasNext][next][hasNext][next]2 true"); + } + + @Override + protected Configuration createConfiguration() throws Exception { + Configuration cfg = super.createConfiguration(); + cfg.setSharedVariable("seq", new MonitoredTemplateSequenceModel(1, 2, 3)); + 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)); + return cfg; + } + + public static abstract class ListContainingTemplateModel { + protected final List<Number> elements; + + public ListContainingTemplateModel(Number... elements) { + this.elements = new ArrayList<Number>(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(); + } + } + +} diff --git a/src/test/java/freemarker/core/ListWithStreamLikeBuiltinsTest.java b/src/test/java/freemarker/core/ListWithStreamLikeBuiltinsTest.java index 1438304..f025e9a 100644 --- a/src/test/java/freemarker/core/ListWithStreamLikeBuiltinsTest.java +++ b/src/test/java/freemarker/core/ListWithStreamLikeBuiltinsTest.java @@ -36,7 +36,7 @@ public class ListWithStreamLikeBuiltinsTest extends TemplateTest { @Test public void testLambdaScope() throws Exception { - // The loop variables aren't visible during the lazy processing done for the elements + // Loop variables aren't visible during the lazy result generation: assertOutput("<#list (1..3)?map(p -> p * 10 + it!'-') as it>${it}<#sep>, </#list>", "10-, 20-, 30-"); assertOutput("<#list (1..3)?map(p -> p * 10 + it_has_next!'-') as it>${it}<#sep>, </#list>",
