| /* |
| * 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; |
| } |
| |
| } |
| |
| } |