/**********************************************************************
* 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.interpret;
import static com.hubspot.jinjava.util.Logging.ENGINE_LOG;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.Stack;
import org.apache.commons.lang3.StringUtils;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
import com.hubspot.jinjava.Jinjava;
import com.hubspot.jinjava.JinjavaConfig;
import com.hubspot.jinjava.el.ExpressionResolver;
import com.hubspot.jinjava.tree.Node;
import com.hubspot.jinjava.tree.TreeParser;
import com.hubspot.jinjava.tree.output.BlockPlaceholderOutputNode;
import com.hubspot.jinjava.tree.output.OutputList;
import com.hubspot.jinjava.tree.output.OutputNode;
import com.hubspot.jinjava.util.Variable;
import com.hubspot.jinjava.util.WhitespaceUtils;
public class JinjavaInterpreter {
private final Multimap<String, List<? extends Node>> blocks = ArrayListMultimap.create();
private final LinkedList<Node> extendParentRoots = new LinkedList<>();
private Context context;
private final JinjavaConfig config;
private final ExpressionResolver expressionResolver;
private final Jinjava application;
private int lineNumber = -1;
private final List<TemplateError> errors = new LinkedList<>();
public JinjavaInterpreter(Jinjava application, Context context, JinjavaConfig renderConfig) {
this.context = context;
this.config = renderConfig;
this.application = application;
this.expressionResolver = new ExpressionResolver(this, application.getExpressionFactory());
}
public JinjavaInterpreter(JinjavaInterpreter orig) {
this(orig.application, new Context(orig.context), orig.config);
}
/**
* @deprecated use {{@link #getConfig()}}
*/
@Deprecated
public JinjavaConfig getConfiguration() {
return config;
}
public void addExtendParentRoot(Node root) {
extendParentRoots.add(root);
}
public void addBlock(String name, LinkedList<? extends Node> value) {
blocks.put(name, value);
}
/**
* Creates a new variable scope, extending from the current scope. Allows you to create a nested
* contextual scope which can override variables from higher levels.
*
* Should be used in a try/finally context, similar to lock-use patterns:
*
* <code>
* interpreter.enterScope();
* try (interpreter.enterScope()) {
* // ...
* }
* </code>
*/
public InterpreterScopeClosable enterScope() {
return enterScope(null);
}
public InterpreterScopeClosable enterScope(Map<Context.Library, Set<String>> disabled) {
context = new Context(context, null, disabled);
return new InterpreterScopeClosable();
}
public void leaveScope() {
Context parent = context.getParent();
if (parent != null) {
parent.addDependencies(context.getDependencies());
context = parent;
}
}
public class InterpreterScopeClosable implements AutoCloseable {
@Override
public void close() {
leaveScope();
}
}
public Node parse(String template) {
return new TreeParser(this, template).buildTree();
}
/**
* Parse the given string into a root Node, and then render it without processing any extend parents.
* This method should be used when the template is known to not have any extends or block tags.
*
* @param template
* string to parse
* @return rendered result
*/
public String renderFlat(String template) {
int depth = context.getRenderDepth();
try {
if (depth > config.getMaxRenderDepth()) {
ENGINE_LOG.warn("Max render depth exceeded: {}", Integer.toString(depth));
return template;
} else {
context.setRenderDepth(depth + 1);
return render(parse(template), false);
}
} finally {
context.setRenderDepth(depth);
}
}
/**
* Parse the given string into a root Node, and then renders it processing extend parents.
*
* @param template
* string to parse
* @return rendered result
*/
public String render(String template) {
ENGINE_LOG.debug(template);
return render(parse(template), true);
}
/**
* Render the given root node, processing extend parents. Equivalent to render(root, true)
*
* @param root
* node to render
* @return rendered result
*/
public String render(Node root) {
return render(root, true);
}
/**
* Render the given root node using this interpreter's current context
*
* @param root
* node to render
* @param processExtendRoots
* if true, also render all extend parents
* @return rendered result
*/
public String render(Node root, boolean processExtendRoots) {
OutputList output = new OutputList(config.getMaxOutputSize());
for (Node node : root.getChildren()) {
lineNumber = node.getLineNumber();
OutputNode out = node.render(this);
output.addNode(out);
}
// render all extend parents, keeping the last as the root output
if (processExtendRoots) {
while (!extendParentRoots.isEmpty()) {
Node parentRoot = extendParentRoots.removeFirst();
output = new OutputList(config.getMaxOutputSize());
for (Node node : parentRoot.getChildren()) {
OutputNode out = node.render(this);
output.addNode(out);
}
context.getExtendPathStack().pop();
}
}
resolveBlockStubs(output);
return output.getValue();
}
private void resolveBlockStubs(OutputList output) {
resolveBlockStubs(output, new Stack<>());
}
private void resolveBlockStubs(OutputList output, Stack<String> blockNames) {
for (BlockPlaceholderOutputNode blockPlaceholder : output.getBlocks()) {
if (!blockNames.contains(blockPlaceholder.getBlockName())) {
Collection<List<? extends Node>> blockChain = blocks.get(blockPlaceholder.getBlockName());
List<? extends Node> block = Iterables.getFirst(blockChain, null);
if (block != null) {
List<? extends Node> superBlock = Iterables.get(blockChain, 1, null);
context.setSuperBlock(superBlock);
OutputList blockValueBuilder = new OutputList(config.getMaxOutputSize());
for (Node child : block) {
blockValueBuilder.addNode(child.render(this));
}
blockNames.push(blockPlaceholder.getBlockName());
resolveBlockStubs(blockValueBuilder, blockNames);
blockNames.pop();
context.removeSuperBlock();
blockPlaceholder.resolve(blockValueBuilder.getValue());
}
}
if (!blockPlaceholder.isResolved()) {
blockPlaceholder.resolve("");
}
}
}
/**
* Resolve a variable from the interpreter context, returning null if not found. This method updates the template error accumulators when a variable is not found.
*
* @param variable
* name of variable in context
* @param lineNumber
* current line number, for error reporting
* @return resolved value for variable
*/
public Object retraceVariable(String variable, int lineNumber) {
if (StringUtils.isBlank(variable)) {
return "";
}
Variable var = new Variable(this, variable);
String varName = var.getName();
Object obj = context.get(varName);
if (obj != null) {
obj = var.resolve(obj);
}
return obj;
}
/**
* Resolve a variable into an object value. If given a string literal (e.g. 'foo' or "foo"), this method returns the literal unquoted. If the variable is undefined in the context, this method returns the given variable string.
*
* @param variable
* name of variable in context
* @param lineNumber
* current line number, for error reporting
* @return resolved value for variable
*/
public Object resolveObject(String variable, int lineNumber) {
if (StringUtils.isBlank(variable)) {
return "";
}
if (WhitespaceUtils.isQuoted(variable)) {
return WhitespaceUtils.unquote(variable);
} else {
Object val = retraceVariable(variable, lineNumber);
if (val == null) {
return variable;
}
return val;
}
}
/**
* Resolve a variable into a string value. If given a string literal (e.g. 'foo' or "foo"), this method returns the literal unquoted. If the variable is undefined in the context, this method returns the given variable string.
*
* @param variable
* name of variable in context
* @param lineNumber
* current line number, for error reporting
* @return resolved value for variable
*/
public String resolveString(String variable, int lineNumber) {
return Objects.toString(resolveObject(variable, lineNumber), "");
}
public Context getContext() {
return context;
}
public String getResource(String resource) throws IOException {
return application.getResourceLocator().getString(resource, config.getCharset(), this);
}
public JinjavaConfig getConfig() {
return config;
}
/**
* Resolve expression against current context.
*
* @param expression
* Jinja expression.
* @param lineNumber
* Line number of expression.
* @return Value of expression.
*/
public Object resolveELExpression(String expression, int lineNumber) {
this.lineNumber = lineNumber;
return expressionResolver.resolveExpression(expression);
}
/**
* Resolve property of bean.
*
* @param object
* Bean.
* @param propertyName
* Name of property to resolve.
* @return Value of property.
*/
public Object resolveProperty(Object object, String propertyName) {
return resolveProperty(object, Collections.singletonList(propertyName));
}
/**
* Resolve property of bean.
*
* @param object
* Bean.
* @param propertyNames
* Names of properties to resolve recursively.
* @return Value of property.
*/
public Object resolveProperty(Object object, List<String> propertyNames) {
return expressionResolver.resolveProperty(object, propertyNames);
}
public int getLineNumber() {
return lineNumber;
}
public void addError(TemplateError templateError) {
this.errors.add(templateError);
}
public List<TemplateError> getErrors() {
return errors;
}
private static final ThreadLocal<Stack<JinjavaInterpreter>> CURRENT_INTERPRETER = ThreadLocal.withInitial(Stack::new);
public static JinjavaInterpreter getCurrent() {
if (CURRENT_INTERPRETER.get().isEmpty()) {
return null;
}
return CURRENT_INTERPRETER.get().peek();
}
public static Optional<JinjavaInterpreter> getCurrentMaybe() {
return Optional.ofNullable(getCurrent());
}
public static void pushCurrent(JinjavaInterpreter interpreter) {
CURRENT_INTERPRETER.get().push(interpreter);
}
public static void popCurrent() {
if (!CURRENT_INTERPRETER.get().isEmpty()) {
CURRENT_INTERPRETER.get().pop();
}
}
}