blob: 2a4df44fcd7fb5ca5e66c9ce4d834dc00adccb34 [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.TemplateModel;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateModelIterator;
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 listExp;
private final String loopVarName;
private final boolean isForEach;
/**
* @param listExp
* a variable referring to a sequence or collection ("the list" from now on)
* @param loopVarName
* The name of the variable that will hold the value of the current item when looping through the list.
* @param childrenBeforeElse
* The nested content to execute if the list wasn't empty; can't be {@code null}. If the loop variable
* was specified in the start tag, this is also what we will iterator over.
*/
IteratorBlock(Expression listExp,
String loopVarName,
TemplateElements childrenBeforeElse,
boolean isForEach) {
this.listExp = listExp;
this.loopVarName = loopVarName;
setChildren(childrenBeforeElse);
this.isForEach = isForEach;
}
@Override
TemplateElement[] accept(Environment env) throws TemplateException, IOException {
acceptWithResult(env);
return null;
}
boolean acceptWithResult(Environment env) throws TemplateException, IOException {
TemplateModel listValue = listExp.eval(env);
if (listValue == null) {
if (env.isClassicCompatible()) {
listValue = Constants.EMPTY_SEQUENCE;
} else {
listExp.assertNonNull(null, env);
}
}
return env.visitIteratorBlock(new IterationContext(listValue, loopVarName));
}
/**
* @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()))) {
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 (isForEach) {
buf.append(_CoreStringUtils.toFTLTopLevelIdentifierReference(loopVarName));
buf.append(" in ");
buf.append(listExp.getCanonicalForm());
} else {
buf.append(listExp.getCanonicalForm());
if (loopVarName != null) {
buf.append(" as ");
buf.append(_CoreStringUtils.toFTLTopLevelIdentifierReference(loopVarName));
}
}
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 loopVarName != null ? 2 : 1;
}
@Override
Object getParameterValue(int idx) {
switch (idx) {
case 0:
return listExp;
case 1:
if (loopVarName == null) throw new IndexOutOfBoundsException();
return loopVarName;
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;
default: throw new IndexOutOfBoundsException();
}
}
@Override
String getNodeTypeSymbol() {
return isForEach ? "#foreach" : "#list";
}
@Override
boolean isNestedBlockRepeater() {
return loopVarName != 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 TemplateModelIterator openedIteratorModel;
private boolean hasNext;
private TemplateModel loopVar;
private int index;
private boolean alreadyEntered;
private Collection localVarNames = null;
/** If the {@code #list} has nested {@code #items}, it's {@code null} outside the {@code #items}. */
private String loopVarName;
private final TemplateModel listValue;
public IterationContext(TemplateModel listValue, String loopVariableName) {
this.listValue = listValue;
this.loopVarName = loopVariableName;
}
boolean accept(Environment env) throws TemplateException, IOException {
return executeNestedContent(env, getChildBuffer());
}
void loopForItemsElement(Environment env, TemplateElement[] childBuffer, String loopVarName)
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;
executeNestedContent(env, childBuffer);
} finally {
this.loopVarName = null;
}
}
/**
* Executes the given block for the {@link #listValue}: if {@link #loopVarName} is non-{@code null}, then for
* each list item once, otherwise once if {@link #listValue} isn't empty.
*/
private boolean executeNestedContent(Environment env, TemplateElement[] childBuffer)
throws TemplateModelException, TemplateException, IOException, NonSequenceOrCollectionException,
InvalidReferenceException {
final boolean listNotEmpty;
if (listValue instanceof TemplateCollectionModel) {
final TemplateCollectionModel collModel = (TemplateCollectionModel) listValue;
final TemplateModelIterator iterModel
= openedIteratorModel == null ? collModel.iterator() : openedIteratorModel;
hasNext = iterModel.hasNext();
listNotEmpty = hasNext;
if (listNotEmpty) {
if (loopVarName != null) {
try {
while (hasNext) {
loopVar = iterModel.next();
hasNext = iterModel.hasNext();
env.visit(childBuffer);
index++;
}
} catch (BreakInstruction.Break br) {
// Silently exit loop
}
openedIteratorModel = null;
} else {
// We must reuse this later, because TemplateCollectionModel-s that wrap an Iterator only
// allow one iterator() call.
openedIteratorModel = iterModel;
env.visit(childBuffer);
}
}
} else if (listValue instanceof TemplateSequenceModel) {
final TemplateSequenceModel seqModel = (TemplateSequenceModel) listValue;
final int size = seqModel.size();
listNotEmpty = size != 0;
if (listNotEmpty) {
if (loopVarName != null) {
try {
for (index = 0; index < size; index++) {
loopVar = seqModel.get(index);
hasNext = (size > index + 1);
env.visit(childBuffer);
}
} catch (BreakInstruction.Break br) {
// Silently exit loop
}
} else {
env.visit(childBuffer);
}
}
} else if (env.isClassicCompatible()) {
listNotEmpty = true;
if (loopVarName != null) {
loopVar = listValue;
hasNext = false;
}
try {
env.visit(childBuffer);
} catch (BreakInstruction.Break br) {
// Silently exit "loop"
}
} else {
throw new NonSequenceOrCollectionException(
listExp, listValue, env);
}
return listNotEmpty;
}
String getLoopVariableName() {
return this.loopVarName;
}
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;
}
}
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;
}
}
}