blob: f9e19c9c1c68afe16f06dd88c2d5bc4926185ab0 [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.Collection;
import java.util.Collections;
import freemarker.template.SimpleNumber;
import freemarker.template.TemplateBooleanModel;
import freemarker.template.TemplateCollectionModel;
import freemarker.template.TemplateException;
import freemarker.template.TemplateHashModelEx;
import freemarker.template.TemplateHashModelEx2;
import freemarker.template.TemplateHashModelEx2.KeyValuePair;
import freemarker.template.TemplateHashModelEx2.KeyValuePairIterator;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelIterator;
import freemarker.template.TemplateScalarModel;
import freemarker.template.TemplateSequenceModel;
import freemarker.template.utility.Constants;
/**
* A #list (or #foreach) element, or pre-#else section of it inside a {@link ListElseContainer}.
*/
final class IteratorBlock extends TemplateElement {
private final Expression listedExp;
private final String loopVar1Name;
private final String loopVar2Name;
private final boolean hashListing;
private final boolean forEach;
/**
* @param listedExp
* a variable referring to a sequence or collection or extended hash to list
* @param loopVar1Name
* The name of the variable that will hold the value of the current item when looping through listed value,
* or {@code null} if we have a nested {@code #items}. If this is a hash listing then this variable will holds the value
* of the hash key.
* @param loopVar2Name
* The name of the variable that will hold the value of the current item when looping through the list,
* or {@code null} if we have a nested {@code #items}. If this is a hash listing then it variable will hold the value
* from the key-value pair.
* @param childrenBeforeElse
* The nested content to execute if the listed value wasn't empty; can't be {@code null}. If the loop variable
* was specified in the start tag, this is also what we will iterate over.
* @param hashListing
* Whether this is a key-value pair listing, or a usual listing. This is properly set even if we have
* a nested {@code #items}.
* @param forEach
* Whether this is {@code #foreach} or a {@code #list}.
*/
IteratorBlock(Expression listedExp,
String loopVar1Name,
String loopVar2Name,
TemplateElements childrenBeforeElse,
boolean hashListing,
boolean forEach) {
this.listedExp = listedExp;
this.loopVar1Name = loopVar1Name;
this.loopVar2Name = loopVar2Name;
setChildren(childrenBeforeElse);
this.hashListing = hashListing;
this.forEach = forEach;
listedExp.enableLazilyGeneratedResult();
}
boolean isHashListing() {
return hashListing;
}
@Override
TemplateElement[] accept(Environment env) throws TemplateException, IOException {
acceptWithResult(env);
return null;
}
boolean acceptWithResult(Environment env) throws TemplateException, IOException {
TemplateModel listedValue = listedExp.eval(env);
if (listedValue == null) {
if (env.isClassicCompatible()) {
listedValue = Constants.EMPTY_SEQUENCE;
} else {
listedExp.assertNonNull(null, env);
}
}
return env.visitIteratorBlock(new IterationContext(listedValue, loopVar1Name, loopVar2Name));
}
@Override
protected String dump(boolean canonical) {
StringBuilder buf = new StringBuilder();
if (canonical) buf.append('<');
buf.append(getNodeTypeSymbol());
buf.append(' ');
if (forEach) {
buf.append(_CoreStringUtils.toFTLTopLevelIdentifierReference(loopVar1Name));
buf.append(" in ");
buf.append(listedExp.getCanonicalForm());
} else {
buf.append(listedExp.getCanonicalForm());
if (loopVar1Name != null) {
buf.append(" as ");
buf.append(_CoreStringUtils.toFTLTopLevelIdentifierReference(loopVar1Name));
if (loopVar2Name != null) {
buf.append(", ");
buf.append(_CoreStringUtils.toFTLTopLevelIdentifierReference(loopVar2Name));
}
}
}
if (canonical) {
buf.append(">");
buf.append(getChildrenCanonicalForm());
if (!(getParentElement() instanceof ListElseContainer)) {
buf.append("</");
buf.append(getNodeTypeSymbol());
buf.append('>');
}
}
return buf.toString();
}
@Override
int getParameterCount() {
return 1 + (loopVar1Name != null ? 1 : 0) + (loopVar2Name != null ? 1 : 0);
}
@Override
Object getParameterValue(int idx) {
switch (idx) {
case 0:
return listedExp;
case 1:
if (loopVar1Name == null) throw new IndexOutOfBoundsException();
return loopVar1Name;
case 2:
if (loopVar2Name == null) throw new IndexOutOfBoundsException();
return loopVar2Name;
default: throw new IndexOutOfBoundsException();
}
}
@Override
ParameterRole getParameterRole(int idx) {
switch (idx) {
case 0:
return ParameterRole.LIST_SOURCE;
case 1:
if (loopVar1Name == null) throw new IndexOutOfBoundsException();
return ParameterRole.TARGET_LOOP_VARIABLE;
case 2:
if (loopVar2Name == null) throw new IndexOutOfBoundsException();
return ParameterRole.TARGET_LOOP_VARIABLE;
default: throw new IndexOutOfBoundsException();
}
}
@Override
String getNodeTypeSymbol() {
return forEach ? "#foreach" : "#list";
}
@Override
boolean isNestedBlockRepeater() {
return loopVar1Name != null;
}
/**
* Holds the context of a #list (or #forEach) directive.
*/
class IterationContext implements LocalContext {
private static final String LOOP_STATE_HAS_NEXT = "_has_next"; // lenght: 9
private static final String LOOP_STATE_INDEX = "_index"; // length 6
private Object openedIterator;
private boolean hasNext;
private TemplateModel loopVar1Value;
private TemplateModel loopVar2Value;
private int index;
private boolean alreadyEntered;
private Collection<String> localVarNames = null;
/**
* The name of the 1st loop variable.
* If the {@code #list} has nested {@code #items}, it's {@code null} outside the {@code #items}.
* Do not use this to resolve {@link LocalContext#getLocalVariable(String)} and such, as the loop variable might
* be still out of scope in FTL when this is already filled; use {@link #visibleLoopVar1Name} for that
* instead.
*/
private String loopVar1Name;
/**
* The name of the 1st loop variable in the {@link LocalContext}. Either {@code null} or {@link #loopVar1Name}.
* When {@code null}, none of the loop variables are in scope in FTL.
* It would be more intuitive if the {@link LocalContext} is not in the local stack when they aren't visible,
* but the {@link LocalContext} is also used for {@code #items} to find its parent, for which we need the tricky
* scoping of the local context stack {@link Environment#getLocalContextStack()}.
*
* (It would be cleaner to have
* {@code boolean loopVarsVisible} instead, but it's a trick to decrease runtime overhead added because of
* lambdas. Certainly an unmeasurable difference... yet it just doesn't feel right when new features slows
* down every existing template a tiny bit, so we try to mitigate that effect.)
*
* @since 2.3.29
*/
private String visibleLoopVar1Name;
/*
* The name of the 2nd loop variable, only used if we list key-value pairs.
* Do not use this to resolve {@link LocalContext#getLocalVariable} and such, when {@link
* #localContextLoopVar1Name} is {@code null}, as then this is not yet in scope as FTL variable.
*/
private String loopVar2Name;
private final TemplateModel listedValue;
public IterationContext(TemplateModel listedValue, String loopVar1Name, String loopVar2Name) {
this.listedValue = listedValue;
this.loopVar1Name = loopVar1Name;
this.loopVar2Name = loopVar2Name;
}
boolean accept(Environment env) throws TemplateException, IOException {
return executeNestedContent(env, getChildBuffer());
}
void loopForItemsElement(Environment env, TemplateElement[] childBuffer, String loopVarName, String loopVar2Name)
throws TemplateException, IOException {
try {
if (alreadyEntered) {
throw new _MiscTemplateException(env,
"The #items directive was already entered earlier for this listing.");
}
alreadyEntered = true;
this.loopVar1Name = loopVarName;
this.loopVar2Name = loopVar2Name;
executeNestedContent(env, childBuffer);
} finally {
this.loopVar1Name = null;
this.loopVar2Name = null;
}
}
/**
* Executes the given block for the {@link #listedValue}: if {@link #loopVar1Name} is non-{@code null}, then for
* each list item once, otherwise once if {@link #listedValue} isn't empty.
*/
private boolean executeNestedContent(Environment env, TemplateElement[] childBuffer)
throws TemplateException, IOException {
return !hashListing
? executedNestedContentForCollOrSeqListing(env, childBuffer)
: executedNestedContentForHashListing(env, childBuffer);
}
private boolean executedNestedContentForCollOrSeqListing(Environment env, TemplateElement[] childBuffer)
throws IOException, TemplateException {
final boolean listNotEmpty;
if (listedValue instanceof TemplateCollectionModel) {
final TemplateCollectionModel collModel = (TemplateCollectionModel) listedValue;
final TemplateModelIterator iterModel
= openedIterator == null ? collModel.iterator()
: ((TemplateModelIterator) openedIterator);
listNotEmpty = iterModel.hasNext();
if (listNotEmpty) {
if (loopVar1Name != null) {
listLoop: do {
loopVar1Value = iterModel.next();
hasNext = iterModel.hasNext();
try {
visibleLoopVar1Name = loopVar1Name; // Makes all loop variables visible in FTL
env.visit(childBuffer);
} catch (BreakOrContinueException br) {
if (br == BreakOrContinueException.BREAK_INSTANCE) {
break listLoop;
}
} finally {
visibleLoopVar1Name = null; // Hides all loop variables in FTL
}
index++;
} while (hasNext);
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 result generation.)
openedIterator = iterModel;
// Note: Loop variables will only become visible inside #items
env.visit(childBuffer);
}
}
} else if (listedValue instanceof TemplateSequenceModel) {
final TemplateSequenceModel seqModel = (TemplateSequenceModel) listedValue;
final int size = seqModel.size();
listNotEmpty = size != 0;
if (listNotEmpty) {
if (loopVar1Name != null) {
listLoop: for (index = 0; index < size; index++) {
loopVar1Value = seqModel.get(index);
hasNext = (size > index + 1);
try {
visibleLoopVar1Name = loopVar1Name; // Makes all loop variables visible in FTL
env.visit(childBuffer);
} catch (BreakOrContinueException br) {
if (br == BreakOrContinueException.BREAK_INSTANCE) {
break listLoop;
}
} finally {
visibleLoopVar1Name = null; // Hides all loop variables in FTL
}
}
} else {
// Note: Loop variables will only become visible inside #items
env.visit(childBuffer);
}
}
} else if (env.isClassicCompatible()) {
listNotEmpty = true;
if (loopVar1Name != null) {
loopVar1Value = listedValue;
hasNext = false;
}
try {
visibleLoopVar1Name = loopVar1Name; // Makes all loop variables visible in FTL
env.visit(childBuffer);
} catch (BreakOrContinueException br) {
// Silently exit "loop"
} finally {
visibleLoopVar1Name = null; // Hides all loop variables in FTL
}
} else if (listedValue instanceof TemplateHashModelEx
&& !NonSequenceOrCollectionException.isWrappedIterable(listedValue)) {
throw new NonSequenceOrCollectionException(env,
new _ErrorDescriptionBuilder("The value you try to list is ",
new _DelayedAOrAn(new _DelayedFTLTypeDescription(listedValue)),
", thus you must specify two loop variables after the \"as\"; one for the key, and "
+ "another for the value, like ", "<#... as k, v>", ")."
));
} else {
throw new NonSequenceOrCollectionException(
listedExp, listedValue, env);
}
return listNotEmpty;
}
private boolean executedNestedContentForHashListing(Environment env, TemplateElement[] childBuffer)
throws IOException, TemplateException {
final boolean hashNotEmpty;
if (listedValue instanceof TemplateHashModelEx) {
TemplateHashModelEx listedHash = (TemplateHashModelEx) listedValue;
if (listedHash instanceof TemplateHashModelEx2) {
KeyValuePairIterator kvpIter
= openedIterator == null ? ((TemplateHashModelEx2) listedHash).keyValuePairIterator()
: (KeyValuePairIterator) openedIterator;
hashNotEmpty = kvpIter.hasNext();
if (hashNotEmpty) {
if (loopVar1Name != null) {
listLoop: do {
KeyValuePair kvp = kvpIter.next();
loopVar1Value = kvp.getKey();
loopVar2Value = kvp.getValue();
hasNext = kvpIter.hasNext();
try {
visibleLoopVar1Name = loopVar1Name; // Makes all loop variables visible in FTL
env.visit(childBuffer);
} catch (BreakOrContinueException br) {
if (br == BreakOrContinueException.BREAK_INSTANCE) {
break listLoop;
}
} finally {
visibleLoopVar1Name = null; // Hides all loop variables in FTL
}
index++;
} while (hasNext);
openedIterator = null;
} else {
// We will reuse this at #items
openedIterator = kvpIter;
// Note: Loop variables will only become visible inside #items
env.visit(childBuffer);
}
}
} else { // not a TemplateHashModelEx2, but still a TemplateHashModelEx
TemplateModelIterator keysIter = listedHash.keys().iterator();
hashNotEmpty = keysIter.hasNext();
if (hashNotEmpty) {
if (loopVar1Name != null) {
listLoop: do {
loopVar1Value = keysIter.next();
if (!(loopVar1Value instanceof TemplateScalarModel)) {
throw _MessageUtil.newKeyValuePairListingNonStringKeyExceptionMessage(
loopVar1Value, (TemplateHashModelEx) listedValue);
}
loopVar2Value = listedHash.get(((TemplateScalarModel) loopVar1Value).getAsString());
hasNext = keysIter.hasNext();
try {
visibleLoopVar1Name = loopVar1Name; // Makes all loop variables visible in FTL
env.visit(childBuffer);
} catch (BreakOrContinueException br) {
if (br == BreakOrContinueException.BREAK_INSTANCE) {
break listLoop;
}
} finally {
visibleLoopVar1Name = null; // Hides all loop variables in FTL
}
index++;
} while (hasNext);
} else {
// Note: Loop variables will only become visible inside #items
env.visit(childBuffer);
}
}
}
} else if (listedValue instanceof TemplateCollectionModel
|| listedValue instanceof TemplateSequenceModel) {
throw new NonSequenceOrCollectionException(env,
new _ErrorDescriptionBuilder("The value you try to list is ",
new _DelayedAOrAn(new _DelayedFTLTypeDescription(listedValue)),
", thus you must specify only one loop variable after the \"as\" (there's no separate "
+ "key and value)."
));
} else {
throw new NonExtendedHashException(
listedExp, listedValue, env);
}
return hashNotEmpty;
}
boolean hasVisibleLoopVar(String visibleLoopVarName) {
String visibleLoopVar1Name = this.visibleLoopVar1Name;
if (visibleLoopVar1Name == null) {
return false; // Loop vars aren't in scope in FTL
}
return visibleLoopVarName.equals(visibleLoopVar1Name) || visibleLoopVarName.equals(loopVar2Name);
}
@Override
public TemplateModel getLocalVariable(String name) {
String visibleLoopVar1Name = this.visibleLoopVar1Name; // Not this.loopVar1Name!
if (visibleLoopVar1Name == null) {
// Loop variables aren't yet in scope in FTL
return null;
}
if (name.startsWith(visibleLoopVar1Name)) {
switch(name.length() - visibleLoopVar1Name.length()) {
case 0:
return loopVar1Value != null ? loopVar1Value
: getTemplate().getConfiguration().getFallbackOnNullLoopVariable()
? null : TemplateNullModel.INSTANCE;
case 6:
if (name.endsWith(LOOP_STATE_INDEX)) {
return new SimpleNumber(index);
}
break;
case 9:
if (name.endsWith(LOOP_STATE_HAS_NEXT)) {
return hasNext ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
}
break;
}
}
if (name.equals(loopVar2Name)) {
return loopVar2Value != null ? loopVar2Value
: getTemplate().getConfiguration().getFallbackOnNullLoopVariable()
? null : TemplateNullModel.INSTANCE;
}
return null;
}
@Override
public Collection<String> getLocalVariableNames() {
String visibleLoopVar1Name = this.visibleLoopVar1Name; // Not this.loopVar1Name!
if (visibleLoopVar1Name != null) {
if (localVarNames == null) {
localVarNames = new ArrayList(3);
localVarNames.add(visibleLoopVar1Name);
localVarNames.add(visibleLoopVar1Name + LOOP_STATE_INDEX);
localVarNames.add(visibleLoopVar1Name + LOOP_STATE_HAS_NEXT);
}
return localVarNames;
} else {
return Collections.emptyList();
}
}
boolean hasNext() {
return hasNext;
}
int getIndex() {
return index;
}
}
}