blob: a42d999ce7366093ad05f71c6a7a07291421ba54 [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.util.Collections;
import java.util.List;
import freemarker.template.TemplateCollectionModel;
import freemarker.template.TemplateException;
import freemarker.template.TemplateMethodModel;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateModelIterator;
import freemarker.template.TemplateSequenceModel;
/**
* 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.
*/
abstract class IntermediateStreamOperationLikeBuiltIn extends BuiltInWithParseTimeParameters {
private Expression elementTransformerExp;
private ElementTransformer precreatedElementTransformer;
private boolean lazilyGeneratedResultEnabled;
@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);
}
Expression elementTransformerExp = parameters.get(0);
setElementTransformerExp(elementTransformerExp);
}
private void setElementTransformerExp(Expression elementTransformerExp) throws ParseException {
this.elementTransformerExp = elementTransformerExp;
if (this.elementTransformerExp instanceof LocalLambdaExpression) {
LocalLambdaExpression localLambdaExp = (LocalLambdaExpression) this.elementTransformerExp;
checkLocalLambdaParamCount(localLambdaExp, 1);
// We can't do this with other kind of expressions, like a function or method reference, as they
// need to be evaluated on runtime:
precreatedElementTransformer = new LocalLambdaElementTransformer(localLambdaExp);
}
}
@Override
protected final boolean isLocalLambdaParameterSupported() {
return true;
}
@Override
final void enableLazilyGeneratedResult() {
this.lazilyGeneratedResultEnabled = true;
}
/** Tells if {@link #enableLazilyGeneratedResult()} was called. */
protected final boolean isLazilyGeneratedResultEnabled() {
return lazilyGeneratedResultEnabled;
}
@Override
protected void setTarget(Expression target) {
super.setTarget(target);
target.enableLazilyGeneratedResult();
}
@Override
protected List<Expression> getArgumentsAsList() {
return Collections.singletonList(elementTransformerExp);
}
@Override
protected int getArgumentsCount() {
return 1;
}
@Override
protected Expression getArgumentParameterValue(int argIdx) {
if (argIdx != 0) {
throw new IndexOutOfBoundsException();
}
return elementTransformerExp;
}
protected Expression getElementTransformerExp() {
return elementTransformerExp;
}
@Override
protected void cloneArguments(
Expression clone, String replacedIdentifier, Expression replacement, ReplacemenetState replacementState) {
try {
((IntermediateStreamOperationLikeBuiltIn) clone).setElementTransformerExp(
elementTransformerExp.deepCloneWithIdentifierReplaced(
replacedIdentifier, replacement, replacementState));
} catch (ParseException e) {
throw new BugException("Deep-clone elementTransformerExp failed", e);
}
}
@Override
TemplateModel _eval(Environment env) throws TemplateException {
TemplateModel targetValue = target.eval(env);
final TemplateModelIterator targetIterator;
final boolean targetIsSequence;
{
if (targetValue instanceof TemplateCollectionModel) {
targetIterator = isLazilyGeneratedResultEnabled()
? new LazyCollectionTemplateModelIterator((TemplateCollectionModel) targetValue)
: ((TemplateCollectionModel) targetValue).iterator();
targetIsSequence = targetValue instanceof LazilyGeneratedCollectionModel
? ((LazilyGeneratedCollectionModel) targetValue).isSequence()
: targetValue instanceof TemplateSequenceModel;
} else if (targetValue instanceof TemplateSequenceModel) {
targetIterator = new LazySequenceIterator((TemplateSequenceModel) targetValue);
targetIsSequence = true;
} else {
throw new NonSequenceOrCollectionException(target, targetValue, env);
}
}
return calculateResult(
targetIterator, targetValue, targetIsSequence,
evalElementTransformerExp(env),
env);
}
private ElementTransformer evalElementTransformerExp(Environment env) throws TemplateException {
if (precreatedElementTransformer != null) {
return precreatedElementTransformer;
}
TemplateModel elementTransformerModel = elementTransformerExp.eval(env);
if (elementTransformerModel instanceof TemplateMethodModel) {
return new MethodElementTransformer((TemplateMethodModel) elementTransformerModel);
} else if (elementTransformerModel instanceof Macro) {
return new FunctionElementTransformer((Macro) elementTransformerModel, elementTransformerExp);
} else {
throw new NonMethodException(elementTransformerExp, elementTransformerModel, true, true, null, env);
}
}
/**
* @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 lhoIsSequence See {@link LazilyGeneratedCollectionModel#isSequence}
* @param elementTransformer The argument to the built-in (typically a lambda expression)
*
* @return {@link TemplateSequenceModel} or {@link TemplateCollectionModel} or {@link TemplateModelIterator}.
*/
protected abstract TemplateModel calculateResult(
TemplateModelIterator lhoIterator, TemplateModel lho, boolean lhoIsSequence,
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;
public LocalLambdaElementTransformer(LocalLambdaExpression elementTransformerExp) {
this.elementTransformerExp = elementTransformerExp;
}
@Override
public TemplateModel transformElement(TemplateModel element, Environment env) throws TemplateException {
return elementTransformerExp.invokeLambdaDefinedFunction(element, env);
}
}
/** {@link ElementTransformer} that wraps a (Java) method call. */
private static class MethodElementTransformer implements ElementTransformer {
private final TemplateMethodModel elementTransformer;
public MethodElementTransformer(TemplateMethodModel elementTransformer) {
this.elementTransformer = elementTransformer;
}
@Override
public TemplateModel transformElement(TemplateModel element, Environment env)
throws TemplateModelException {
Object result = elementTransformer.exec(Collections.singletonList(element));
return result instanceof TemplateModel ? (TemplateModel) result : env.getObjectWrapper().wrap(result);
}
}
/** {@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;
public FunctionElementTransformer(Macro templateTransformer, Expression elementTransformerExp) {
this.templateTransformer = templateTransformer;
this.elementTransformerExp = elementTransformerExp;
}
@Override
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,
Collections.singletonList(functionArgExp),
elementTransformerExp);
}
}
}