blob: 1bb2b9ae90da38013fa7741dd32f107de5934a22 [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 org.apache.freemarker.core;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import org.apache.freemarker.core.model.TemplateBooleanModel;
import org.apache.freemarker.core.model.TemplateHashModelEx;
import org.apache.freemarker.core.model.TemplateIterableModel;
import org.apache.freemarker.core.model.TemplateModel;
import org.apache.freemarker.core.model.TemplateModelIterator;
import org.apache.freemarker.core.model.TemplateNodeModel;
import org.apache.freemarker.core.model.TemplateNullModel;
import org.apache.freemarker.core.model.impl.SimpleNumber;
import org.apache.freemarker.core.util._StringUtils;
/**
* AST directive node: {@code #list} element, or pre-{@code #else} section of it inside a
* {@link ASTDirListElseContainer}.
*/
final class ASTDirList extends ASTDirective {
private final ASTExpression listedExp;
private final String nestedContentParam1Name;
private final String nestedContentParam2Name;
private final boolean hashListing;
/**
* @param listedExp
* a variable referring to an iterable or extended hash that we want to list
* @param nestedContentParam1Name
* 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 nestedContentParam2Name
* 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
* nested content parameter is 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}.
*/
ASTDirList(ASTExpression listedExp,
String nestedContentParam1Name,
String nestedContentParam2Name,
TemplateElements childrenBeforeElse,
boolean hashListing) {
this.listedExp = listedExp;
this.nestedContentParam1Name = nestedContentParam1Name;
this.nestedContentParam2Name = nestedContentParam2Name;
setChildren(childrenBeforeElse);
this.hashListing = hashListing;
}
boolean isHashListing() {
return hashListing;
}
@Override
ASTElement[] execute(Environment env) throws TemplateException, IOException {
acceptWithResult(env);
return null;
}
boolean acceptWithResult(Environment env) throws TemplateException, IOException {
TemplateModel listedValue = listedExp.eval(env);
if (listedValue == null) {
listedExp.assertNonNull(null, env);
}
return env.visitIteratorBlock(new IterationContext(listedValue, nestedContentParam1Name, nestedContentParam2Name));
}
@Override
String dump(boolean canonical) {
StringBuilder buf = new StringBuilder();
if (canonical) buf.append('<');
buf.append(getLabelWithoutParameters());
buf.append(' ');
buf.append(listedExp.getCanonicalForm());
if (nestedContentParam1Name != null) {
buf.append(" as ");
buf.append(_StringUtils.toFTLTopLevelIdentifierReference(nestedContentParam1Name));
if (nestedContentParam2Name != null) {
buf.append(", ");
buf.append(_StringUtils.toFTLTopLevelIdentifierReference(nestedContentParam2Name));
}
}
if (canonical) {
buf.append(">");
buf.append(getChildrenCanonicalForm());
if (!(getParent() instanceof ASTDirListElseContainer)) {
buf.append("</");
buf.append(getLabelWithoutParameters());
buf.append('>');
}
}
return buf.toString();
}
@Override
int getParameterCount() {
return 1 + (nestedContentParam1Name != null ? 1 : 0) + (nestedContentParam2Name != null ? 1 : 0);
}
@Override
Object getParameterValue(int idx) {
switch (idx) {
case 0:
return listedExp;
case 1:
if (nestedContentParam1Name == null) throw new IndexOutOfBoundsException();
return nestedContentParam1Name;
case 2:
if (nestedContentParam2Name == null) throw new IndexOutOfBoundsException();
return nestedContentParam2Name;
default: throw new IndexOutOfBoundsException();
}
}
@Override
ParameterRole getParameterRole(int idx) {
switch (idx) {
case 0:
return ParameterRole.LIST_SOURCE;
case 1:
if (nestedContentParam1Name == null) throw new IndexOutOfBoundsException();
return ParameterRole.NESTED_CONTENT_PARAMETER;
case 2:
if (nestedContentParam2Name == null) throw new IndexOutOfBoundsException();
return ParameterRole.NESTED_CONTENT_PARAMETER;
default: throw new IndexOutOfBoundsException();
}
}
@Override
public String getLabelWithoutParameters() {
return "#list";
}
@Override
boolean isNestedBlockRepeater() {
return nestedContentParam1Name != null;
}
/**
* Holds the context of a #list 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 nestedContentParam;
private TemplateModel nestedContentParam2;
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 nestedContentParam1Name;
/** Used if we list key-value pairs */
private String nestedContentParam2Name;
/**
* Whether the nested content parameters are visible from the template at the moment.
* 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()}.
*/
private boolean nestedContentParamsVisible;
private final TemplateModel listedValue;
public IterationContext(TemplateModel listedValue,
String nestedContentParamName, String nestedContentParam2Name) {
this.listedValue = listedValue;
this.nestedContentParam1Name = nestedContentParamName;
this.nestedContentParam2Name = nestedContentParam2Name;
}
boolean accept(Environment env) throws TemplateException, IOException {
return executeNestedContent(env, getChildBuffer());
}
void loopForItemsElement(Environment env, ASTElement[] childBuffer,
String nestedContentParamName, String nestedContentParam2Name)
throws TemplateException, IOException {
try {
if (alreadyEntered) {
throw new TemplateException(env,
"The #items directive was already entered earlier for this listing.");
}
alreadyEntered = true;
this.nestedContentParam1Name = nestedContentParamName;
this.nestedContentParam2Name = nestedContentParam2Name;
executeNestedContent(env, childBuffer);
} finally {
this.nestedContentParam1Name = null;
this.nestedContentParam2Name = null;
}
}
/**
* Executes the given block for the {@link #listedValue}: if {@link #nestedContentParam1Name} is non-{@code
* null}, then for each list item once, otherwise once if {@link #listedValue} isn't empty.
*/
private boolean executeNestedContent(Environment env, ASTElement[] childBuffer)
throws TemplateException, IOException {
return !hashListing
? executedNestedContentForIterableListing(env, childBuffer)
: executedNestedContentForHashListing(env, childBuffer);
}
private boolean executedNestedContentForIterableListing(Environment env, ASTElement[] childBuffer)
throws IOException, TemplateException {
final boolean listNotEmpty;
if (listedValue instanceof TemplateIterableModel) {
final TemplateIterableModel collModel = (TemplateIterableModel) listedValue;
final TemplateModelIterator iterModel
= openedIterator == null ? collModel.iterator()
: ((TemplateModelIterator) openedIterator);
listNotEmpty = iterModel.hasNext();
if (listNotEmpty) {
if (nestedContentParam1Name != null) {
listLoop: do {
nestedContentParam = iterModel.next();
hasNext = iterModel.hasNext();
try {
nestedContentParamsVisible = true;
env.executeElements(childBuffer);
} catch (BreakOrContinueException br) {
if (br == BreakOrContinueException.BREAK_INSTANCE) {
break listLoop;
}
} finally {
nestedContentParamsVisible = false;
}
index++;
} while (hasNext);
openedIterator = null;
} else {
// We must reuse this later, because some TemplateIterableModel can only allow one iterator()
// call (such as those wrapping an Iterator).
openedIterator = iterModel;
// Note: Nested content parameters will only become visible inside #items
env.executeElements(childBuffer);
}
}
} else if (listedValue instanceof TemplateHashModelEx) {
throw new TemplateException(env,
new _ErrorDescriptionBuilder("The value you try to list is ",
new _DelayedAOrAn(new _DelayedTemplateLanguageTypeDescription(listedValue)),
", thus you must declare two nested content parameters after the \"as\"; one for the "
+ "key, and another for the value, like ", "<#... as k, v>", ")."
));
} else {
throw MessageUtils.newUnexpectedOperandTypeException(
listedExp, listedValue,
MessageUtils.EXPECTED_TYPE_ITERABLE_DESC,
TemplateIterableModel.class,
null, env);
}
return listNotEmpty;
}
private boolean executedNestedContentForHashListing(Environment env, ASTElement[] childBuffer)
throws IOException, TemplateException {
final boolean hashNotEmpty;
if (listedValue instanceof TemplateHashModelEx) {
TemplateHashModelEx listedHash = (TemplateHashModelEx) listedValue;
TemplateHashModelEx.KeyValuePairIterator kvpIter
= openedIterator == null ? listedHash.keyValuePairIterator()
: (TemplateHashModelEx.KeyValuePairIterator) openedIterator;
hashNotEmpty = kvpIter.hasNext();
if (hashNotEmpty) {
if (nestedContentParam1Name != null) {
listLoop: do {
TemplateHashModelEx.KeyValuePair kvp = kvpIter.next();
nestedContentParam = kvp.getKey();
nestedContentParam2 = kvp.getValue();
hasNext = kvpIter.hasNext();
try {
nestedContentParamsVisible = true;
env.executeElements(childBuffer);
} catch (BreakOrContinueException br) {
if (br == BreakOrContinueException.BREAK_INSTANCE) {
break listLoop;
}
} finally {
nestedContentParamsVisible = false;
}
index++;
} while (hasNext);
openedIterator = null;
} else {
// We will reuse this at the #iterms
openedIterator = kvpIter;
// Note: Nested content parameters will only become visible inside #items
env.executeElements(childBuffer);
}
}
} else if (listedValue instanceof TemplateIterableModel) {
throw new TemplateException(env,
new _ErrorDescriptionBuilder("The value you try to list is ",
new _DelayedAOrAn(new _DelayedTemplateLanguageTypeDescription(listedValue)),
", thus you must declare only one nested content parameter after the \"as\" (there's "
+ "no separate key and value)."
));
} else {
throw MessageUtils.newUnexpectedOperandTypeException(
listedExp, listedValue, TemplateHashModelEx.class, env);
}
return hashNotEmpty;
}
String getNestedContentParameter1Name() {
return nestedContentParam1Name;
}
String getNestedContentParameter2Name() {
return nestedContentParam2Name;
}
@Override
public TemplateModel getLocalVariable(String name) {
if (!nestedContentParamsVisible) {
return null;
}
String nestedContentParamName = this.nestedContentParam1Name;
if (name.startsWith(nestedContentParamName)) {
switch(name.length() - nestedContentParamName.length()) {
case 0:
// TODO [FM3][null] Later nestedContentParam == null will mean undefined, which should be an error
return nestedContentParam != null ? nestedContentParam : 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(nestedContentParam2Name)) {
return nestedContentParam2 != null ? nestedContentParam2 : TemplateNullModel.INSTANCE;
}
return null;
}
@Override
public Collection<String> getLocalVariableNames() {
if (!nestedContentParamsVisible) {
return Collections.EMPTY_LIST;
}
String nestedContentParamName = this.nestedContentParam1Name;
if (localVarNames == null) {
localVarNames = new ArrayList(3);
localVarNames.add(nestedContentParamName);
localVarNames.add(nestedContentParamName + LOOP_STATE_INDEX);
localVarNames.add(nestedContentParamName + LOOP_STATE_HAS_NEXT);
}
return localVarNames;
}
boolean hasNext() {
return hasNext;
}
int getIndex() {
return index;
}
}
}