/*
* 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.TemplateModelException;
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 ListElseContainer
. /**
* 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 loopVarName;
private final String loopVar2Name;
private final boolean hashListing;
private final boolean forEach;
Params: - listedExp –
a variable referring to a sequence or collection or extended hash to list
- loopVarName – The name of the variable that will hold the value of the current item when looping through listed value, or
null
if we have a nested #items
. If this is a hash listing then this variable will holds the value of the hash key. - loopVar2Name – The name of the variable that will hold the value of the current item when looping through the list, or
null
if we have a nested #items
. If this is a hash listing then it variable will hold the value from the key-value pair. - childrenBeforeElse – The nested content to execute if the listed value wasn't empty; can't be
null
. If the loop variable was specified in the start tag, this is also what we will iterate over. - hashListing – Whether this is a key-value pair listing, or a usual listing. This is properly set even if we have a nested
#items
. - forEach – Whether this is
#foreach
or a #list
.
/**
* @param listedExp
* a variable referring to a sequence or collection or extended hash to list
* @param loopVarName
* 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 loopVarName,
String loopVar2Name,
TemplateElements childrenBeforeElse,
boolean hashListing,
boolean forEach) {
this.listedExp = listedExp;
this.loopVarName = loopVarName;
this.loopVar2Name = loopVar2Name;
setChildren(childrenBeforeElse);
this.hashListing = hashListing;
this.forEach = forEach;
}
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, loopVarName, loopVar2Name));
}
Params: - loopVariableName – Then name of the loop variable whose context we are looking for, or
null
if we simply look for the innermost context.
Returns: The matching context or null
if no such context exists.
/**
* @param loopVariableName
* Then name of the loop variable whose context we are looking for, or {@code null} if we simply look for
* the innermost context.
* @return The matching context or {@code null} if no such context exists.
*/
static IterationContext findEnclosingIterationContext(Environment env, String loopVariableName)
throws _MiscTemplateException {
LocalContextStack ctxStack = env.getLocalContextStack();
if (ctxStack != null) {
for (int i = ctxStack.size() - 1; i >= 0; i--) {
Object ctx = ctxStack.get(i);
if (ctx instanceof IterationContext
&& (loopVariableName == null
|| loopVariableName.equals(((IterationContext) ctx).getLoopVariableName())
|| loopVariableName.equals(((IterationContext) ctx).getLoopVariable2Name())
)) {
return (IterationContext) ctx;
}
}
}
return null;
}
@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(loopVarName));
buf.append(" in ");
buf.append(listedExp.getCanonicalForm());
} else {
buf.append(listedExp.getCanonicalForm());
if (loopVarName != null) {
buf.append(" as ");
buf.append(_CoreStringUtils.toFTLTopLevelIdentifierReference(loopVarName));
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 + (loopVarName != null ? 1 : 0) + (loopVar2Name != null ? 1 : 0);
}
@Override
Object getParameterValue(int idx) {
switch (idx) {
case 0:
return listedExp;
case 1:
if (loopVarName == null) throw new IndexOutOfBoundsException();
return loopVarName;
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 (loopVarName == 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 loopVarName != null;
}
Holds the context of a #list (or #forEach) directive.
/**
* 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 loopVar;
private TemplateModel loopVar2;
private int index;
private boolean alreadyEntered;
private Collection localVarNames = null;
If the #list
has nested #items
, it's null
outside the #items
. /** If the {@code #list} has nested {@code #items}, it's {@code null} outside the {@code #items}. */
private String loopVarName;
Used if we list key-value pairs /** Used if we list key-value pairs */
private String loopVar2Name;
private final TemplateModel listedValue;
public IterationContext(TemplateModel listedValue, String loopVarName, String loopVar2Name) {
this.listedValue = listedValue;
this.loopVarName = loopVarName;
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 NonSequenceOrCollectionException, TemplateModelException, InvalidReferenceException,
TemplateException, IOException {
try {
if (alreadyEntered) {
throw new _MiscTemplateException(env,
"The #items directive was already entered earlier for this listing.");
}
alreadyEntered = true;
this.loopVarName = loopVarName;
this.loopVar2Name = loopVar2Name;
executeNestedContent(env, childBuffer);
} finally {
this.loopVarName = null;
this.loopVar2Name = null;
}
}
Executes the given block for the listedValue
: if loopVarName
is non-null
, then for each list item once, otherwise once if listedValue
isn't empty. /**
* Executes the given block for the {@link #listedValue}: if {@link #loopVarName} 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 TemplateModelException, TemplateException, IOException, NonSequenceOrCollectionException,
InvalidReferenceException {
return !hashListing
? executedNestedContentForCollOrSeqListing(env, childBuffer)
: executedNestedContentForHashListing(env, childBuffer);
}
private boolean executedNestedContentForCollOrSeqListing(Environment env, TemplateElement[] childBuffer)
throws TemplateModelException, IOException, TemplateException,
NonSequenceOrCollectionException, InvalidReferenceException {
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 (loopVarName != null) {
listLoop: do {
loopVar = iterModel.next();
hasNext = iterModel.hasNext();
try {
env.visit(childBuffer);
} catch (BreakOrContinueException br) {
if (br == BreakOrContinueException.BREAK_INSTANCE) {
break listLoop;
}
}
index++;
} while (hasNext);
openedIterator = null;
} else {
// We must reuse this later, because TemplateCollectionModel-s that wrap an Iterator only
// allow one iterator() call.
openedIterator = iterModel;
env.visit(childBuffer);
}
}
} else if (listedValue instanceof TemplateSequenceModel) {
final TemplateSequenceModel seqModel = (TemplateSequenceModel) listedValue;
final int size = seqModel.size();
listNotEmpty = size != 0;
if (listNotEmpty) {
if (loopVarName != null) {
listLoop: for (index = 0; index < size; index++) {
loopVar = seqModel.get(index);
hasNext = (size > index + 1);
try {
env.visit(childBuffer);
} catch (BreakOrContinueException br) {
if (br == BreakOrContinueException.BREAK_INSTANCE) {
break listLoop;
}
}
}
} else {
env.visit(childBuffer);
}
}
} else if (env.isClassicCompatible()) {
listNotEmpty = true;
if (loopVarName != null) {
loopVar = listedValue;
hasNext = false;
}
try {
env.visit(childBuffer);
} catch (BreakOrContinueException br) {
// Silently exit "loop"
}
} 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 TemplateModelException, 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 (loopVarName != null) {
listLoop: do {
KeyValuePair kvp = kvpIter.next();
loopVar = kvp.getKey();
loopVar2 = kvp.getValue();
hasNext = kvpIter.hasNext();
try {
env.visit(childBuffer);
} catch (BreakOrContinueException br) {
if (br == BreakOrContinueException.BREAK_INSTANCE) {
break listLoop;
}
}
index++;
} while (hasNext);
openedIterator = null;
} else {
// We will reuse this at the #iterms
openedIterator = kvpIter;
env.visit(childBuffer);
}
}
} else { // not a TemplateHashModelEx2, but still a TemplateHashModelEx
TemplateModelIterator keysIter = listedHash.keys().iterator();
hashNotEmpty = keysIter.hasNext();
if (hashNotEmpty) {
if (loopVarName != null) {
listLoop: do {
loopVar = keysIter.next();
if (!(loopVar instanceof TemplateScalarModel)) {
throw _MessageUtil.newKeyValuePairListingNonStringKeyExceptionMessage(
loopVar, (TemplateHashModelEx) listedValue);
}
loopVar2 = listedHash.get(((TemplateScalarModel) loopVar).getAsString());
hasNext = keysIter.hasNext();
try {
env.visit(childBuffer);
} catch (BreakOrContinueException br) {
if (br == BreakOrContinueException.BREAK_INSTANCE) {
break listLoop;
}
}
index++;
} while (hasNext);
} else {
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;
}
String getLoopVariableName() {
return this.loopVarName;
}
String getLoopVariable2Name() {
return this.loopVar2Name;
}
public TemplateModel getLocalVariable(String name) {
String loopVariableName = this.loopVarName;
if (loopVariableName != null && name.startsWith(loopVariableName)) {
switch(name.length() - loopVariableName.length()) {
case 0:
return loopVar;
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 loopVar2;
}
return null;
}
public Collection getLocalVariableNames() {
String loopVariableName = this.loopVarName;
if (loopVariableName != null) {
if (localVarNames == null) {
localVarNames = new ArrayList(3);
localVarNames.add(loopVariableName);
localVarNames.add(loopVariableName + LOOP_STATE_INDEX);
localVarNames.add(loopVariableName + LOOP_STATE_HAS_NEXT);
}
return localVarNames;
} else {
return Collections.EMPTY_LIST;
}
}
boolean hasNext() {
return hasNext;
}
int getIndex() {
return index;
}
}
}