Skip to content

Commit 815fdfd

Browse files
authored
Allow partial evaluation of templates (#282)
* Defer evaluation of parts of templates * Handle deferring a whole TagNode * Add test for preserving function invocations * Also defer evaluation when encountering a random value * Update javadocs to use @link
1 parent 8de4520 commit 815fdfd

8 files changed

Lines changed: 310 additions & 10 deletions

File tree

src/main/java/com/hubspot/jinjava/el/ExpressionResolver.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.hubspot.jinjava.interpret.TemplateSyntaxException;
2424
import com.hubspot.jinjava.interpret.UnknownTokenException;
2525
import com.hubspot.jinjava.interpret.errorcategory.BasicTemplateErrorCategory;
26+
import com.hubspot.jinjava.interpret.DeferredValueException;
2627
import com.hubspot.jinjava.lib.fn.ELFunctionDefinition;
2728

2829
import de.odysseus.el.tree.TreeBuilderException;
@@ -84,12 +85,18 @@ public Object resolveExpression(String expression) {
8485
interpreter.addError(TemplateError.fromException(new TemplateSyntaxException(expression.substring(e.getPosition() - EXPRESSION_START_TOKEN.length()),
8586
"Error parsing '" + expression + "': " + errorMessage, interpreter.getLineNumber(), position, e)));
8687
} catch (ELException e) {
88+
if (e.getCause() != null && e.getCause() instanceof DeferredValueException) {
89+
throw (DeferredValueException) e.getCause();
90+
}
8791
interpreter.addError(TemplateError.fromException(new TemplateSyntaxException(expression, e.getMessage(), interpreter.getLineNumber(), e)));
8892
} catch (DisabledException e) {
8993
interpreter.addError(new TemplateError(ErrorType.FATAL, ErrorReason.DISABLED, ErrorItem.FUNCTION, e.getMessage(), expression, interpreter.getLineNumber(), interpreter.getPosition(), e));
9094
} catch (UnknownTokenException e) {
9195
// Re-throw the exception because you only get this when the config failOnUnknownTokens is enabled.
9296
throw e;
97+
} catch (DeferredValueException e) {
98+
// Re-throw so that it can be handled in JinjavaInterpreter
99+
throw e;
93100
} catch (Exception e) {
94101
interpreter.addError(TemplateError.fromException(new InterpretException(
95102
String.format("Error resolving expression [%s]: " + getRootCauseMessage(e), expression), e, interpreter.getLineNumber(), interpreter.getPosition())));
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.hubspot.jinjava.interpret;
2+
3+
/**
4+
* Marker object which indicates that the template engine should skip over evaluating
5+
* this part of the template, if the object is resolved from the context.
6+
*
7+
*/
8+
public class DeferredValue {
9+
private static final DeferredValue INSTANCE = new DeferredValue();
10+
11+
private DeferredValue() {
12+
}
13+
14+
public static DeferredValue instance() {
15+
return INSTANCE;
16+
}
17+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.hubspot.jinjava.interpret;
2+
3+
/**
4+
* Exception thrown when attempting to render a {@link com.hubspot.jinjava.interpret.DeferredValue}.
5+
* The exception is effectively used for flow control, to unwind evaluating a template Node
6+
* and instead echo its contents to the output.
7+
*/
8+
public class DeferredValueException extends InterpretException {
9+
public DeferredValueException(String message) {
10+
super("Encountered a deferred value: " + message);
11+
}
12+
13+
public DeferredValueException(String variable, int lineNumber, int startPosition) {
14+
super("Encountered a deferred value: \"" + variable + "\"", lineNumber, startPosition);
15+
}
16+
}

src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
import com.hubspot.jinjava.interpret.TemplateError.ErrorType;
4747
import com.hubspot.jinjava.interpret.errorcategory.BasicTemplateErrorCategory;
4848
import com.hubspot.jinjava.random.ConstantZeroRandomNumberGenerator;
49-
import com.hubspot.jinjava.random.RandomNumberGeneratorStrategy;
49+
import com.hubspot.jinjava.random.DeferredRandomNumberGenerator;
5050
import com.hubspot.jinjava.tree.Node;
5151
import com.hubspot.jinjava.tree.TreeParser;
5252
import com.hubspot.jinjava.tree.output.BlockPlaceholderOutputNode;
@@ -79,12 +79,18 @@ public JinjavaInterpreter(Jinjava application, Context context, JinjavaConfig re
7979
this.config = renderConfig;
8080
this.application = application;
8181

82-
if (config.getRandomNumberGeneratorStrategy() == RandomNumberGeneratorStrategy.THREAD_LOCAL) {
83-
random = ThreadLocalRandom.current();
84-
} else if (config.getRandomNumberGeneratorStrategy() == RandomNumberGeneratorStrategy.CONSTANT_ZERO) {
85-
random = new ConstantZeroRandomNumberGenerator();
86-
} else {
87-
throw new IllegalStateException("No random number generator with strategy " + config.getRandomNumberGeneratorStrategy());
82+
switch (config.getRandomNumberGeneratorStrategy()) {
83+
case THREAD_LOCAL:
84+
random = ThreadLocalRandom.current();
85+
break;
86+
case CONSTANT_ZERO:
87+
random = new ConstantZeroRandomNumberGenerator();
88+
break;
89+
case DEFERRED:
90+
random = new DeferredRandomNumberGenerator();
91+
break;
92+
default:
93+
throw new IllegalStateException("No random number generator with strategy " + config.getRandomNumberGeneratorStrategy());
8894
}
8995

9096
this.expressionResolver = new ExpressionResolver(this, application.getExpressionFactory());
@@ -234,8 +240,13 @@ public String render(Node root, boolean processExtendRoots) {
234240
null, BasicTemplateErrorCategory.IMPORT_CYCLE_DETECTED, ImmutableMap.of("string", renderStr)));
235241
output.addNode(new RenderedOutputNode(renderStr));
236242
} else {
243+
OutputNode out;
237244
context.pushRenderStack(renderStr);
238-
OutputNode out = node.render(this);
245+
try {
246+
out = node.render(this);
247+
} catch (DeferredValueException e) {
248+
out = new RenderedOutputNode(node.getMaster().getImage());
249+
}
239250
context.popRenderStack();
240251
output.addNode(out);
241252
}
@@ -317,6 +328,9 @@ public Object retraceVariable(String variable, int lineNumber, int startPosition
317328
String varName = var.getName();
318329
Object obj = context.get(varName);
319330
if (obj != null) {
331+
if (obj instanceof DeferredValue) {
332+
throw new DeferredValueException(variable, lineNumber, startPosition);
333+
}
320334
obj = var.resolve(obj);
321335
} else if (getConfig().isFailOnUnknownTokens()) {
322336
throw new UnknownTokenException(variable, lineNumber, startPosition);
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package com.hubspot.jinjava.random;
2+
3+
import java.util.Random;
4+
import java.util.stream.DoubleStream;
5+
import java.util.stream.IntStream;
6+
import java.util.stream.LongStream;
7+
8+
import com.hubspot.jinjava.interpret.DeferredValueException;
9+
10+
/**
11+
* A random number generator that throws {@link com.hubspot.jinjava.interpret.DeferredValueException} for all supported methods.
12+
*/
13+
public class DeferredRandomNumberGenerator extends Random {
14+
15+
private static final String EXCEPTION_MESSAGE = "Generating random number";
16+
17+
@Override
18+
protected int next(int bits) {
19+
throw new DeferredValueException(EXCEPTION_MESSAGE);
20+
}
21+
22+
@Override
23+
public int nextInt() {
24+
throw new DeferredValueException(EXCEPTION_MESSAGE);
25+
}
26+
27+
@Override
28+
public int nextInt(int bound) {
29+
throw new DeferredValueException(EXCEPTION_MESSAGE);
30+
}
31+
32+
@Override
33+
public long nextLong() {
34+
throw new DeferredValueException(EXCEPTION_MESSAGE);
35+
}
36+
37+
@Override
38+
public boolean nextBoolean() {
39+
throw new DeferredValueException(EXCEPTION_MESSAGE);
40+
}
41+
42+
@Override
43+
public float nextFloat() {
44+
throw new DeferredValueException(EXCEPTION_MESSAGE);
45+
}
46+
47+
@Override
48+
public double nextDouble() {
49+
throw new DeferredValueException(EXCEPTION_MESSAGE);
50+
}
51+
52+
@Override
53+
public synchronized double nextGaussian() {
54+
throw new DeferredValueException(EXCEPTION_MESSAGE);
55+
}
56+
57+
@Override
58+
public void nextBytes(byte[] bytes) {
59+
throw new UnsupportedOperationException();
60+
}
61+
62+
@Override
63+
public IntStream ints(long streamSize) {
64+
throw new UnsupportedOperationException();
65+
}
66+
67+
@Override
68+
public IntStream ints() {
69+
throw new UnsupportedOperationException();
70+
}
71+
72+
@Override
73+
public IntStream ints(long streamSize, int randomNumberOrigin, int randomNumberBound) {
74+
throw new UnsupportedOperationException();
75+
}
76+
77+
@Override
78+
public IntStream ints(int randomNumberOrigin, int randomNumberBound) {
79+
throw new UnsupportedOperationException();
80+
}
81+
82+
@Override
83+
public LongStream longs(long streamSize) {
84+
throw new UnsupportedOperationException();
85+
}
86+
87+
@Override
88+
public LongStream longs() {
89+
throw new UnsupportedOperationException();
90+
}
91+
92+
@Override
93+
public LongStream longs(long streamSize, long randomNumberOrigin, long randomNumberBound) {
94+
throw new UnsupportedOperationException();
95+
}
96+
97+
@Override
98+
public LongStream longs(long randomNumberOrigin, long randomNumberBound) {
99+
throw new UnsupportedOperationException();
100+
}
101+
102+
@Override
103+
public DoubleStream doubles(long streamSize) {
104+
throw new UnsupportedOperationException();
105+
}
106+
107+
@Override
108+
public DoubleStream doubles() {
109+
throw new UnsupportedOperationException();
110+
}
111+
112+
@Override
113+
public DoubleStream doubles(long streamSize, double randomNumberOrigin, double randomNumberBound) {
114+
throw new UnsupportedOperationException();
115+
}
116+
117+
@Override
118+
public DoubleStream doubles(double randomNumberOrigin, double randomNumberBound) {
119+
throw new UnsupportedOperationException();
120+
}
121+
}

src/main/java/com/hubspot/jinjava/random/RandomNumberGeneratorStrategy.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22

33
public enum RandomNumberGeneratorStrategy {
44
THREAD_LOCAL,
5-
CONSTANT_ZERO
5+
CONSTANT_ZERO,
6+
DEFERRED
67
}

src/main/java/com/hubspot/jinjava/tree/TagNode.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import com.hubspot.jinjava.interpret.InterpretException;
1919
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
20+
import com.hubspot.jinjava.interpret.DeferredValueException;
2021
import com.hubspot.jinjava.lib.tag.Tag;
2122
import com.hubspot.jinjava.tree.output.OutputNode;
2223
import com.hubspot.jinjava.tree.output.RenderedOutputNode;
@@ -48,13 +49,14 @@ private TagNode(TagNode n) {
4849

4950
@Override
5051
public OutputNode render(JinjavaInterpreter interpreter) {
51-
5252
if (interpreter.getContext().isValidationMode() && !tag.isRenderedInValidationMode()) {
5353
return new RenderedOutputNode("");
5454
}
5555

5656
try {
5757
return tag.interpretOutput(this, interpreter);
58+
} catch (DeferredValueException e) {
59+
return new RenderedOutputNode(reconstructImage());
5860
} catch (InterpretException e) {
5961
throw e;
6062
} catch (Exception e) {
@@ -84,4 +86,17 @@ public Tag getTag() {
8486
return tag;
8587
}
8688

89+
90+
private String reconstructImage() {
91+
StringBuilder builder = new StringBuilder().append(master.getImage());
92+
93+
for (Node n : getChildren()) {
94+
builder.append(n.getMaster().getImage());
95+
}
96+
97+
builder.append("{% ").append(getEndName()). append(" %}");
98+
99+
return builder.toString();
100+
}
101+
87102
}

0 commit comments

Comments
 (0)