Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 43 additions & 3 deletions src/main/java/com/hubspot/jinjava/lib/tag/ForTag.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
* @author anysome
*/
@JinjavaDoc(
value = "Outputs the inner content for each item in the given iterable",
value = "Outputs the inner content for each item in the given iterable, or the else block if the iterable is empty",
params = {
@JinjavaParam(
value = "items_to_iterate",
Expand Down Expand Up @@ -156,6 +156,19 @@ public String renderForCollection(
Object collection
) {
ForLoop loop = ObjectIterator.getLoop(collection);
boolean empty = !loop.hasNext();
List<Node> children = tagNode.getChildren();
int elseIndex = children.size();
for (int i = 0; i < children.size(); i++) {
if (
children.get(i) instanceof TagNode &&
ElseTag.TAG_NAME.equals(children.get(i).getName())
) {
elseIndex = i;
break;
}
}
String result;

try (InterpreterScopeClosable c = interpreter.enterScope()) {
if (interpreter.isValidationMode() && !loop.hasNext()) {
Expand Down Expand Up @@ -257,7 +270,7 @@ public String renderForCollection(
}
}

for (Node node : tagNode.getChildren()) {
for (Node node : children.subList(0, elseIndex)) {
if (interpreter.getContext().isValidationMode()) {
node.render(interpreter);
} else {
Expand All @@ -284,8 +297,35 @@ public String renderForCollection(
throw new DeferredValueException(TOO_LARGE_EXCEPTION_MESSAGE);
}
}
return checkLoopVariable(interpreter, buff);
result = checkLoopVariable(interpreter, buff);
}
if (elseIndex < children.size() && (empty || interpreter.isValidationMode())) {
try (InterpreterScopeClosable c = interpreter.enterScope()) {
if (!empty) {
interpreter.getContext().setValidationMode(true);
}
LengthLimitingStringBuilder buff = new LengthLimitingStringBuilder(
interpreter.getConfig().getMaxOutputSize()
);
for (Node node : children.subList(elseIndex + 1, children.size())) {
if (interpreter.getContext().isValidationMode()) {
node.render(interpreter);
} else {
try {
buff.append(node.render(interpreter));
} catch (OutputTooBigException e) {
if (interpreter.getConfig().getExecutionMode().useEagerParser()) {
throw new DeferredValueException(TOO_LARGE_EXCEPTION_MESSAGE);
}
interpreter.addError(TemplateError.fromOutputTooBigException(e));
break;
}
}
}
result += buff.toString();
}
}
return result;
}

private String checkLoopVariable(
Expand Down
123 changes: 116 additions & 7 deletions src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerForTag.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
import com.hubspot.jinjava.interpret.DeferredValueException;
import com.hubspot.jinjava.interpret.InterpretException;
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
import com.hubspot.jinjava.lib.tag.ElseTag;
import com.hubspot.jinjava.lib.tag.ForTag;
import com.hubspot.jinjava.tree.Node;
import com.hubspot.jinjava.tree.TagNode;
import com.hubspot.jinjava.tree.parse.TagToken;
import com.hubspot.jinjava.util.EagerContextWatcher;
Expand Down Expand Up @@ -153,6 +155,13 @@ public String eagerInterpret(
);
}

for (int i = 0; i < tagNode.getChildren().size(); i++) {
Node child = tagNode.getChildren().get(i);
if (child instanceof TagNode && ElseTag.TAG_NAME.equals(child.getName())) {
return renderDeferredForElse(tagNode, interpreter, result, i);
}
}

EagerExecutionResult firstRunResult = runLoopOnce(tagNode, interpreter, true);
PrefixToPreserveState prefixToPreserveState = firstRunResult
.getPrefixToPreserveState()
Expand Down Expand Up @@ -186,23 +195,123 @@ private EagerExecutionResult runLoopOnce(
TagNode tagNode,
JinjavaInterpreter interpreter,
boolean clearDeferredWords
) {
return runBranchOnce(
tagNode,
interpreter,
clearDeferredWords,
tagNode.getChildren(),
true
);
}

private String renderDeferredForElse(
TagNode tagNode,
JinjavaInterpreter interpreter,
LengthLimitingStringBuilder result,
int elseIndex
) {
List<Node> body = tagNode.getChildren().subList(0, elseIndex);
List<Node> otherwise = tagNode
.getChildren()
.subList(elseIndex + 1, tagNode.getChildren().size());
EagerExecutionResult firstBody = runBranchOnce(
tagNode,
interpreter,
true,
body,
true
);
PrefixToPreserveState prefix = firstBody.getPrefixToPreserveState();
Set<String> bindingsToDefer = new HashSet<>(
EagerReconstructionUtils.resetSpeculativeBindings(interpreter, firstBody)
);
EagerExecutionResult firstElse = runBranchOnce(
tagNode,
interpreter,
true,
otherwise,
false
);
prefix.withAll(firstElse.getPrefixToPreserveState());
bindingsToDefer.addAll(
EagerReconstructionUtils.resetSpeculativeBindings(interpreter, firstElse)
);
prefix.withAllInFront(
EagerReconstructionUtils.deferWordsAndReconstructReferences(
interpreter,
bindingsToDefer
)
);

// Re-evaluate both alternatives after all potentially modified values are deferred.
// Reset each branch before evaluating the other one, just as for eager if branches.
EagerExecutionResult secondBody = runBranchOnce(
tagNode,
interpreter,
false,
body,
true
);
if (!bindingsToDefer.containsAll(secondBody.getSpeculativeBindings().keySet())) {
throw new DeferredValueException("Modified values in deferred for loop");
}
result.append(secondBody.asTemplateString());
prefix.withAllInFront(
EagerReconstructionUtils.resetAndDeferSpeculativeBindings(interpreter, secondBody)
);
result.append(tagNode.getChildren().get(elseIndex).getMaster().getImage());
EagerExecutionResult secondElse = runBranchOnce(
tagNode,
interpreter,
false,
otherwise,
false
);
if (!bindingsToDefer.containsAll(secondElse.getSpeculativeBindings().keySet())) {
throw new DeferredValueException("Modified values in deferred for else block");
}
result.append(secondElse.asTemplateString());
prefix.withAllInFront(
EagerReconstructionUtils.resetAndDeferSpeculativeBindings(interpreter, secondElse)
);
result.append(EagerReconstructionUtils.reconstructEnd(tagNode));
return prefix.toString() + result;
}

private EagerExecutionResult runBranchOnce(
TagNode tagNode,
JinjavaInterpreter interpreter,
boolean clearDeferredWords,
List<Node> children,
boolean loopBody
) {
return EagerContextWatcher.executeInChildContext(
eagerInterpreter -> {
if (!(eagerInterpreter.getContext().get(ForTag.LOOP) instanceof DeferredValue)) {
if (
loopBody &&
!(eagerInterpreter.getContext().get(ForTag.LOOP) instanceof DeferredValue)
) {
eagerInterpreter.getContext().put(ForTag.LOOP, DeferredValue.instance());
}
List<String> loopVars = getTag()
.getLoopVarsAndExpression((TagToken) tagNode.getMaster())
.getLeft();
List<String> loopVars = loopBody
? getTag().getLoopVarsAndExpression((TagToken) tagNode.getMaster()).getLeft()
: Collections.emptyList();
interpreter.getContext().addNonMetaContextVariables(loopVars);
loopVars.forEach(var ->
interpreter.getContext().put(var, DeferredValue.instance())
);
try {
return EagerExpressionResult.fromString(
renderChildren(tagNode, eagerInterpreter)
);
if (children == tagNode.getChildren()) {
return EagerExpressionResult.fromString(
renderChildren(tagNode, eagerInterpreter)
);
}
StringBuilder output = new StringBuilder();
for (Node child : children) {
output.append(child.render(eagerInterpreter).getValue());
}
return EagerExpressionResult.fromString(output.toString());
} finally {
interpreter.getContext().removeNonMetaContextVariables(loopVars);
if (clearDeferredWords) {
Expand Down
Loading