/**********************************************************************
* Copyright (c) 2014 HubSpot Inc.
*
* Licensed 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 com.hubspot.jinjava.lib.tag;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.commons.lang3.StringUtils;
import com.google.common.collect.Lists;
import com.hubspot.jinjava.doc.annotations.JinjavaDoc;
import com.hubspot.jinjava.doc.annotations.JinjavaParam;
import com.hubspot.jinjava.doc.annotations.JinjavaSnippet;
import com.hubspot.jinjava.interpret.InterpretException;
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
import com.hubspot.jinjava.interpret.JinjavaInterpreter.InterpreterScopeClosable;
import com.hubspot.jinjava.interpret.TemplateSyntaxException;
import com.hubspot.jinjava.tree.Node;
import com.hubspot.jinjava.tree.TagNode;
import com.hubspot.jinjava.util.ForLoop;
import com.hubspot.jinjava.util.HelperStringTokenizer;
import com.hubspot.jinjava.util.ObjectIterator;
/**
* {% for a in b|f1:d,c %}
*
* {% for key, value in my_dict.items() %}
*
* @author anysome
*
*/
@JinjavaDoc(
value = "Outputs the inner content for each item in the given iterable",
params = {
@JinjavaParam(value = "items_to_iterate", desc = "Specifies the name of a single item in the sequence or dict."),
},
snippets = {
@JinjavaSnippet(
code = "{% for item in items %}\n" +
" {{ item }}\n" +
"{% endfor %}"),
@JinjavaSnippet(
desc = "Iterating over dictionary values",
code = "{% for value in dictionary %}\n" +
" {{ value }}\n" +
"{% endfor %}"),
@JinjavaSnippet(
desc = "Iterating over dictionary entries",
code = "{% for key, value in dictionary.items() %}\n" +
" {{ key }}: {{ value }}\n" +
"{% endfor %}"),
@JinjavaSnippet(
desc = "Standard blog listing loop",
code = "{% for content in contents %}\n" +
" Post content variables\n" +
"{% endfor %}")
})
public class ForTag implements Tag {
private static final long serialVersionUID = 6175143875754966497L;
private static final String LOOP = "loop";
private static final String TAGNAME = "for";
private static final String ENDTAGNAME = "endfor";
@SuppressWarnings("unchecked")
@Override
public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) {
/* apdlv72@gmail.com
* Fix for issues with for-loops that contain whitespace in their range, e.g.
* "{% for i in range(1 * 1, 2 * 2) %}"
* This is because HelperStringTokenizer will split the range expressions also
* at white spaces and end up with [i, in, range(1, *, 1, 2, *, 2)].
* To avoid this, the below fix will remove white space from the expression
* on the right side of the keyword "in". It will do so however only if there
* are no characters in this expression that indicate strings - namely ' and ".
* This avoids messing up expressions like {% for i in ['a ','b'] %} that
* contain spaces in the arguments.
* TODO A somewhat more sophisticated tokenizing/parsing of the for-loop expression.
*/
String helpers = tagNode.getHelpers();
String parts[] = helpers.split("\\s+in\\s+");
if (2==parts.length && !parts[1].contains("'") && !parts[1].contains("\"") ) {
helpers = parts[0] + " in " + parts[1].replace(" ", "");
}
List<String> helper = new HelperStringTokenizer(helpers).splitComma(true).allTokens();
List<String> loopVars = Lists.newArrayList();
int inPos = 0;
while (inPos < helper.size()) {
String val = helper.get(inPos);
if ("in".equals(val)) {
break;
} else {
loopVars.add(val);
inPos++;
}
}
if (inPos >= helper.size()) {
throw new TemplateSyntaxException(tagNode.getMaster().getImage(), "Tag 'for' expects valid 'in' clause, got: " + tagNode.getHelpers(), tagNode.getLineNumber());
}
String loopExpr = StringUtils.join(helper.subList(inPos + 1, helper.size()), ",");
Object collection = interpreter.resolveELExpression(loopExpr, tagNode.getLineNumber());
ForLoop loop = ObjectIterator.getLoop(collection);
try (InterpreterScopeClosable c = interpreter.enterScope()) {
interpreter.getContext().put(LOOP, loop);
StringBuilder buff = new StringBuilder();
while (loop.hasNext()) {
Object val = loop.next();
// set item variables
if (loopVars.size() == 1) {
interpreter.getContext().put(loopVars.get(0), val);
} else {
for (String loopVar : loopVars) {
if (Map.Entry.class.isAssignableFrom(val.getClass())) {
Map.Entry<String, Object> entry = (Entry<String, Object>) val;
Object entryVal = null;
if (loopVars.indexOf(loopVar) == 0) {
entryVal = entry.getKey();
} else if (loopVars.indexOf(loopVar) == 1) {
entryVal = entry.getValue();
}
interpreter.getContext().put(loopVar, entryVal);
} else {
try {
PropertyDescriptor[] valProps = Introspector.getBeanInfo(val.getClass()).getPropertyDescriptors();
for (PropertyDescriptor valProp : valProps) {
if (loopVar.equals(valProp.getName())) {
interpreter.getContext().put(loopVar, valProp.getReadMethod().invoke(val));
break;
}
}
} catch (Exception e) {
throw new InterpretException(e.getMessage(), e, tagNode.getLineNumber());
}
}
}
}
for (Node node : tagNode.getChildren()) {
buff.append(node.render(interpreter));
}
}
return buff.toString();
}
}
@Override
public String getEndTagName() {
return ENDTAGNAME;
}
@Override
public String getName() {
return TAGNAME;
}
}