Skip to content

Commit

Permalink
Allow partial evaluation of templates (#282)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
pfarrel committed Feb 20, 2019
1 parent 8de4520 commit 815fdfd
Show file tree
Hide file tree
Showing 8 changed files with 310 additions and 10 deletions.
7 changes: 7 additions & 0 deletions src/main/java/com/hubspot/jinjava/el/ExpressionResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import com.hubspot.jinjava.interpret.TemplateSyntaxException;
import com.hubspot.jinjava.interpret.UnknownTokenException;
import com.hubspot.jinjava.interpret.errorcategory.BasicTemplateErrorCategory;
import com.hubspot.jinjava.interpret.DeferredValueException;
import com.hubspot.jinjava.lib.fn.ELFunctionDefinition;

import de.odysseus.el.tree.TreeBuilderException;
Expand Down Expand Up @@ -84,12 +85,18 @@ public Object resolveExpression(String expression) {
interpreter.addError(TemplateError.fromException(new TemplateSyntaxException(expression.substring(e.getPosition() - EXPRESSION_START_TOKEN.length()),
"Error parsing '" + expression + "': " + errorMessage, interpreter.getLineNumber(), position, e)));
} catch (ELException e) {
if (e.getCause() != null && e.getCause() instanceof DeferredValueException) {
throw (DeferredValueException) e.getCause();
}
interpreter.addError(TemplateError.fromException(new TemplateSyntaxException(expression, e.getMessage(), interpreter.getLineNumber(), e)));
} catch (DisabledException e) {
interpreter.addError(new TemplateError(ErrorType.FATAL, ErrorReason.DISABLED, ErrorItem.FUNCTION, e.getMessage(), expression, interpreter.getLineNumber(), interpreter.getPosition(), e));
} catch (UnknownTokenException e) {
// Re-throw the exception because you only get this when the config failOnUnknownTokens is enabled.
throw e;
} catch (DeferredValueException e) {
// Re-throw so that it can be handled in JinjavaInterpreter
throw e;
} catch (Exception e) {
interpreter.addError(TemplateError.fromException(new InterpretException(
String.format("Error resolving expression [%s]: " + getRootCauseMessage(e), expression), e, interpreter.getLineNumber(), interpreter.getPosition())));
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/com/hubspot/jinjava/interpret/DeferredValue.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.hubspot.jinjava.interpret;

/**
* Marker object which indicates that the template engine should skip over evaluating
* this part of the template, if the object is resolved from the context.
*
*/
public class DeferredValue {
private static final DeferredValue INSTANCE = new DeferredValue();

private DeferredValue() {
}

public static DeferredValue instance() {
return INSTANCE;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.hubspot.jinjava.interpret;

/**
* Exception thrown when attempting to render a {@link com.hubspot.jinjava.interpret.DeferredValue}.
* The exception is effectively used for flow control, to unwind evaluating a template Node
* and instead echo its contents to the output.
*/
public class DeferredValueException extends InterpretException {
public DeferredValueException(String message) {
super("Encountered a deferred value: " + message);
}

public DeferredValueException(String variable, int lineNumber, int startPosition) {
super("Encountered a deferred value: \"" + variable + "\"", lineNumber, startPosition);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
import com.hubspot.jinjava.interpret.TemplateError.ErrorType;
import com.hubspot.jinjava.interpret.errorcategory.BasicTemplateErrorCategory;
import com.hubspot.jinjava.random.ConstantZeroRandomNumberGenerator;
import com.hubspot.jinjava.random.RandomNumberGeneratorStrategy;
import com.hubspot.jinjava.random.DeferredRandomNumberGenerator;
import com.hubspot.jinjava.tree.Node;
import com.hubspot.jinjava.tree.TreeParser;
import com.hubspot.jinjava.tree.output.BlockPlaceholderOutputNode;
Expand Down Expand Up @@ -79,12 +79,18 @@ public JinjavaInterpreter(Jinjava application, Context context, JinjavaConfig re
this.config = renderConfig;
this.application = application;

if (config.getRandomNumberGeneratorStrategy() == RandomNumberGeneratorStrategy.THREAD_LOCAL) {
random = ThreadLocalRandom.current();
} else if (config.getRandomNumberGeneratorStrategy() == RandomNumberGeneratorStrategy.CONSTANT_ZERO) {
random = new ConstantZeroRandomNumberGenerator();
} else {
throw new IllegalStateException("No random number generator with strategy " + config.getRandomNumberGeneratorStrategy());
switch (config.getRandomNumberGeneratorStrategy()) {
case THREAD_LOCAL:
random = ThreadLocalRandom.current();
break;
case CONSTANT_ZERO:
random = new ConstantZeroRandomNumberGenerator();
break;
case DEFERRED:
random = new DeferredRandomNumberGenerator();
break;
default:
throw new IllegalStateException("No random number generator with strategy " + config.getRandomNumberGeneratorStrategy());
}

this.expressionResolver = new ExpressionResolver(this, application.getExpressionFactory());
Expand Down Expand Up @@ -234,8 +240,13 @@ public String render(Node root, boolean processExtendRoots) {
null, BasicTemplateErrorCategory.IMPORT_CYCLE_DETECTED, ImmutableMap.of("string", renderStr)));
output.addNode(new RenderedOutputNode(renderStr));
} else {
OutputNode out;
context.pushRenderStack(renderStr);
OutputNode out = node.render(this);
try {
out = node.render(this);
} catch (DeferredValueException e) {
out = new RenderedOutputNode(node.getMaster().getImage());
}
context.popRenderStack();
output.addNode(out);
}
Expand Down Expand Up @@ -317,6 +328,9 @@ public Object retraceVariable(String variable, int lineNumber, int startPosition
String varName = var.getName();
Object obj = context.get(varName);
if (obj != null) {
if (obj instanceof DeferredValue) {
throw new DeferredValueException(variable, lineNumber, startPosition);
}
obj = var.resolve(obj);
} else if (getConfig().isFailOnUnknownTokens()) {
throw new UnknownTokenException(variable, lineNumber, startPosition);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package com.hubspot.jinjava.random;

import java.util.Random;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.LongStream;

import com.hubspot.jinjava.interpret.DeferredValueException;

/**
* A random number generator that throws {@link com.hubspot.jinjava.interpret.DeferredValueException} for all supported methods.
*/
public class DeferredRandomNumberGenerator extends Random {

private static final String EXCEPTION_MESSAGE = "Generating random number";

@Override
protected int next(int bits) {
throw new DeferredValueException(EXCEPTION_MESSAGE);
}

@Override
public int nextInt() {
throw new DeferredValueException(EXCEPTION_MESSAGE);
}

@Override
public int nextInt(int bound) {
throw new DeferredValueException(EXCEPTION_MESSAGE);
}

@Override
public long nextLong() {
throw new DeferredValueException(EXCEPTION_MESSAGE);
}

@Override
public boolean nextBoolean() {
throw new DeferredValueException(EXCEPTION_MESSAGE);
}

@Override
public float nextFloat() {
throw new DeferredValueException(EXCEPTION_MESSAGE);
}

@Override
public double nextDouble() {
throw new DeferredValueException(EXCEPTION_MESSAGE);
}

@Override
public synchronized double nextGaussian() {
throw new DeferredValueException(EXCEPTION_MESSAGE);
}

@Override
public void nextBytes(byte[] bytes) {
throw new UnsupportedOperationException();
}

@Override
public IntStream ints(long streamSize) {
throw new UnsupportedOperationException();
}

@Override
public IntStream ints() {
throw new UnsupportedOperationException();
}

@Override
public IntStream ints(long streamSize, int randomNumberOrigin, int randomNumberBound) {
throw new UnsupportedOperationException();
}

@Override
public IntStream ints(int randomNumberOrigin, int randomNumberBound) {
throw new UnsupportedOperationException();
}

@Override
public LongStream longs(long streamSize) {
throw new UnsupportedOperationException();
}

@Override
public LongStream longs() {
throw new UnsupportedOperationException();
}

@Override
public LongStream longs(long streamSize, long randomNumberOrigin, long randomNumberBound) {
throw new UnsupportedOperationException();
}

@Override
public LongStream longs(long randomNumberOrigin, long randomNumberBound) {
throw new UnsupportedOperationException();
}

@Override
public DoubleStream doubles(long streamSize) {
throw new UnsupportedOperationException();
}

@Override
public DoubleStream doubles() {
throw new UnsupportedOperationException();
}

@Override
public DoubleStream doubles(long streamSize, double randomNumberOrigin, double randomNumberBound) {
throw new UnsupportedOperationException();
}

@Override
public DoubleStream doubles(double randomNumberOrigin, double randomNumberBound) {
throw new UnsupportedOperationException();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@

public enum RandomNumberGeneratorStrategy {
THREAD_LOCAL,
CONSTANT_ZERO
CONSTANT_ZERO,
DEFERRED
}
17 changes: 16 additions & 1 deletion src/main/java/com/hubspot/jinjava/tree/TagNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import com.hubspot.jinjava.interpret.InterpretException;
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
import com.hubspot.jinjava.interpret.DeferredValueException;
import com.hubspot.jinjava.lib.tag.Tag;
import com.hubspot.jinjava.tree.output.OutputNode;
import com.hubspot.jinjava.tree.output.RenderedOutputNode;
Expand Down Expand Up @@ -48,13 +49,14 @@ private TagNode(TagNode n) {

@Override
public OutputNode render(JinjavaInterpreter interpreter) {

if (interpreter.getContext().isValidationMode() && !tag.isRenderedInValidationMode()) {
return new RenderedOutputNode("");
}

try {
return tag.interpretOutput(this, interpreter);
} catch (DeferredValueException e) {
return new RenderedOutputNode(reconstructImage());
} catch (InterpretException e) {
throw e;
} catch (Exception e) {
Expand Down Expand Up @@ -84,4 +86,17 @@ public Tag getTag() {
return tag;
}


private String reconstructImage() {
StringBuilder builder = new StringBuilder().append(master.getImage());

for (Node n : getChildren()) {
builder.append(n.getMaster().getImage());
}

builder.append("{% ").append(getEndName()). append(" %}");

return builder.toString();
}

}

0 comments on commit 815fdfd

Please sign in to comment.