Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow partial evaluation of templates #282

Merged
merged 9 commits into from
Feb 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we could store a reference to the original template string and an index value of the start and end n the node object. I think this would solve the raw case and preserve all the whitespace.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll try to get that working in a separate PR. If I can get it to work, I'll loop back and update this PR to use it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, it looks like it might require a larger version bump because I will need to mess around with Tag / Node etc constructors. I'll go ahead with this for now and come back to that as I refine this.


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

builder.append("{% ").append(getEndName()). append(" %}");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you use/create constants for {% and %}? People have requested to make these configurable and I'd like to consolidate them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure


return builder.toString();
}

}