() {
+ int next = 0;
+
+ public boolean hasNext() {
+ return properties != null && next < properties.length;
+ }
+
+ public FeatureDescriptor next() {
+ PropertyDescriptor property = properties[next++];
+ FeatureDescriptor feature = new FeatureDescriptor();
+ feature.setDisplayName(property.getDisplayName());
+ feature.setName(property.getName());
+ feature.setShortDescription(property.getShortDescription());
+ feature.setExpert(property.isExpert());
+ feature.setHidden(property.isHidden());
+ feature.setPreferred(property.isPreferred());
+ feature.setValue(TYPE, property.getPropertyType());
+ feature.setValue(RESOLVABLE_AT_DESIGN_TIME, true);
+ return feature;
+ }
+
+ public void remove() {
+ throw new UnsupportedOperationException("cannot remove");
+ }
+ };
+ }
+ return null;
+ }
+
+ /**
+ * If the base object is not null, returns the most general acceptable type that can be set on
+ * this bean property. If the base is not null, the propertyResolved property of the ELContext
+ * object must be set to true by this resolver, before returning. If this property is not true
+ * after this method is called, the caller should ignore the return value. The provided property
+ * will first be coerced to a String. If there is a BeanInfoProperty for this property and there
+ * were no errors retrieving it, the propertyType of the propertyDescriptor is returned.
+ * Otherwise, a PropertyNotFoundException is thrown.
+ *
+ * @param context
+ * The context of this evaluation.
+ * @param base
+ * The bean to analyze.
+ * @param property
+ * The name of the property to analyze. Will be coerced to a String.
+ * @return If the propertyResolved property of ELContext was set to true, then the most general
+ * acceptable type; otherwise undefined.
+ * @throws NullPointerException
+ * if context is null
+ * @throws PropertyNotFoundException
+ * if base is not null and the specified property does not exist or is not readable.
+ * @throws ELException
+ * if an exception was thrown while performing the property or variable resolution.
+ * The thrown exception must be included as the cause property of this exception, if
+ * available.
+ */
+ @Override
+ public Class> getType(ELContext context, Object base, Object property) {
+ if (context == null) {
+ throw new NullPointerException();
+ }
+ Class> result = null;
+ if (isResolvable(base)) {
+ result = toBeanProperty(base, property).getPropertyType();
+ context.setPropertyResolved(true);
+ }
+ return result;
+ }
+
+ /**
+ * If the base object is not null, returns the current value of the given property on this bean.
+ * If the base is not null, the propertyResolved property of the ELContext object must be set to
+ * true by this resolver, before returning. If this property is not true after this method is
+ * called, the caller should ignore the return value. The provided property name will first be
+ * coerced to a String. If the property is a readable property of the base object, as per the
+ * JavaBeans specification, then return the result of the getter call. If the getter throws an
+ * exception, it is propagated to the caller. If the property is not found or is not readable, a
+ * PropertyNotFoundException is thrown.
+ *
+ * @param context
+ * The context of this evaluation.
+ * @param base
+ * The bean to analyze.
+ * @param property
+ * The name of the property to analyze. Will be coerced to a String.
+ * @return If the propertyResolved property of ELContext was set to true, then the value of the
+ * given property. Otherwise, undefined.
+ * @throws NullPointerException
+ * if context is null
+ * @throws PropertyNotFoundException
+ * if base is not null and the specified property does not exist or is not readable.
+ * @throws ELException
+ * if an exception was thrown while performing the property or variable resolution.
+ * The thrown exception must be included as the cause property of this exception, if
+ * available.
+ */
+ @Override
+ public Object getValue(ELContext context, Object base, Object property) {
+ if (context == null) {
+ throw new NullPointerException();
+ }
+ Object result = null;
+ if (isResolvable(base)) {
+ Method method = getReadMethod(base, property);
+ if (method == null) {
+ throw new PropertyNotFoundException("Cannot read property " + property);
+ }
+ try {
+ result = method.invoke(base);
+ } catch (InvocationTargetException e) {
+ throw new ELException(e.getCause());
+ } catch (Exception e) {
+ throw new ELException(e);
+ }
+ context.setPropertyResolved(true);
+ }
+ return result;
+ }
+
+ /**
+ * If the base object is not null, returns whether a call to
+ * {@link #setValue(ELContext, Object, Object, Object)} will always fail. If the base is not
+ * null, the propertyResolved property of the ELContext object must be set to true by this
+ * resolver, before returning. If this property is not true after this method is called, the
+ * caller can safely assume no value was set.
+ *
+ * @param context
+ * The context of this evaluation.
+ * @param base
+ * The bean to analyze.
+ * @param property
+ * The name of the property to analyze. Will be coerced to a String.
+ * @return If the propertyResolved property of ELContext was set to true, then true if calling
+ * the setValue method will always fail or false if it is possible that such a call may
+ * succeed; otherwise undefined.
+ * @throws NullPointerException
+ * if context is null
+ * @throws PropertyNotFoundException
+ * if base is not null and the specified property does not exist or is not readable.
+ * @throws ELException
+ * if an exception was thrown while performing the property or variable resolution.
+ * The thrown exception must be included as the cause property of this exception, if
+ * available.
+ */
+ @Override
+ public boolean isReadOnly(ELContext context, Object base, Object property) {
+ if (context == null) {
+ throw new NullPointerException();
+ }
+ boolean result = readOnly;
+ if (isResolvable(base)) {
+ result |= toBeanProperty(base, property).isReadOnly();
+ context.setPropertyResolved(true);
+ }
+ return result;
+ }
+
+ /**
+ * If the base object is not null, attempts to set the value of the given property on this bean.
+ * If the base is not null, the propertyResolved property of the ELContext object must be set to
+ * true by this resolver, before returning. If this property is not true after this method is
+ * called, the caller can safely assume no value was set. If this resolver was constructed in
+ * read-only mode, this method will always throw PropertyNotWritableException. The provided
+ * property name will first be coerced to a String. If property is a writable property of base
+ * (as per the JavaBeans Specification), the setter method is called (passing value). If the
+ * property exists but does not have a setter, then a PropertyNotFoundException is thrown. If
+ * the property does not exist, a PropertyNotFoundException is thrown.
+ *
+ * @param context
+ * The context of this evaluation.
+ * @param base
+ * The bean to analyze.
+ * @param property
+ * The name of the property to analyze. Will be coerced to a String.
+ * @param value
+ * The value to be associated with the specified key.
+ * @throws NullPointerException
+ * if context is null
+ * @throws PropertyNotFoundException
+ * if base is not null and the specified property does not exist or is not readable.
+ * @throws PropertyNotWritableException
+ * if this resolver was constructed in read-only mode, or if there is no setter for
+ * the property
+ * @throws ELException
+ * if an exception was thrown while performing the property or variable resolution.
+ * The thrown exception must be included as the cause property of this exception, if
+ * available.
+ */
+ @Override
+ public void setValue(ELContext context, Object base, Object property, Object value) {
+ if (context == null) {
+ throw new NullPointerException();
+ }
+ if (isResolvable(base)) {
+ if (readOnly) {
+ throw new PropertyNotWritableException("resolver is read-only");
+ }
+ Method method = getWriteMethod(base, property);
+ if (method == null) {
+ throw new PropertyNotWritableException("Cannot write property: " + property);
+ }
+ try {
+ method.invoke(base, value);
+ } catch (InvocationTargetException e) {
+ throw new ELException("Cannot write property: " + property, e.getCause());
+ } catch (IllegalArgumentException e) {
+ throw new ELException("Cannot write property: " + property, e);
+ } catch (IllegalAccessException e) {
+ throw new PropertyNotWritableException("Cannot write property: " + property, e);
+ }
+ context.setPropertyResolved(true);
+ }
+ }
+
+ /**
+ * If the base object is not null, invoke the method, with the given parameters on
+ * this bean. The return value from the method is returned.
+ *
+ *
+ * If the base is not null, the propertyResolved property of the
+ * ELContext object must be set to true by this resolver, before
+ * returning. If this property is not true after this method is called, the caller
+ * should ignore the return value.
+ *
+ *
+ *
+ * The provided method object will first be coerced to a String. The methods in the
+ * bean is then examined and an attempt will be made to select one for invocation. If no
+ * suitable can be found, a MethodNotFoundException is thrown.
+ *
+ * If the given paramTypes is not null, select the method with the given name and
+ * parameter types.
+ *
+ * Else select the method with the given name that has the same number of parameters. If there
+ * are more than one such method, the method selection process is undefined.
+ *
+ * Else select the method with the given name that takes a variable number of arguments.
+ *
+ * Note the resolution for overloaded methods will likely be clarified in a future version of
+ * the spec.
+ *
+ * The provided parameters are coerced to the corresponding parameter types of the method, and
+ * the method is then invoked.
+ *
+ * @param context
+ * The context of this evaluation.
+ * @param base
+ * The bean on which to invoke the method
+ * @param method
+ * The simple name of the method to invoke. Will be coerced to a String.
+ * If method is "<init>"or "<clinit>" a MethodNotFoundException is
+ * thrown.
+ * @param paramTypes
+ * An array of Class objects identifying the method's formal parameter types, in
+ * declared order. Use an empty array if the method has no parameters. Can be
+ * null, in which case the method's formal parameter types are assumed
+ * to be unknown.
+ * @param params
+ * The parameters to pass to the method, or null if no parameters.
+ * @return The result of the method invocation (null if the method has a
+ * void return type).
+ * @throws MethodNotFoundException
+ * if no suitable method can be found.
+ * @throws ELException
+ * if an exception was thrown while performing (base, method) resolution. The thrown
+ * exception must be included as the cause property of this exception, if available.
+ * If the exception thrown is an InvocationTargetException, extract its
+ * cause and pass it to the ELException constructor.
+ * @since 2.2
+ */
+ @Override
+ public Object invoke(
+ ELContext context,
+ Object base,
+ Object method,
+ Class>[] paramTypes,
+ Object[] params
+ ) {
+ if (context == null) {
+ throw new NullPointerException();
+ }
+ Object result = null;
+ if (isResolvable(base)) {
+ if (params == null) {
+ params = new Object[0];
+ }
+ String name = method.toString();
+ Method target = findMethod(base, name, paramTypes, params, params.length);
+ if (target == null) {
+ throw new MethodNotFoundException(
+ "Cannot find method " +
+ name +
+ " with " +
+ params.length +
+ " parameters in " +
+ base.getClass()
+ );
+ }
+ try {
+ result =
+ target.invoke(
+ base,
+ coerceParams(getExpressionFactory(context), target, params)
+ );
+ } catch (InvocationTargetException e) {
+ throw new ELException(e.getCause());
+ } catch (IllegalAccessException e) {
+ throw new ELException(e);
+ }
+ context.setPropertyResolved(true);
+ }
+ return result;
+ }
+
+ // Changed modifier to protected; Added `Object[] params` parameter
+ protected Method findMethod(
+ Object base,
+ String name,
+ Class>[] types,
+ Object[] params,
+ int paramCount
+ ) {
+ if (types != null) {
+ try {
+ return findAccessibleMethod(base.getClass().getMethod(name, types));
+ } catch (NoSuchMethodException e) {
+ return null;
+ }
+ }
+ Method varArgsMethod = null;
+ for (Method method : base.getClass().getMethods()) {
+ if (method.getName().equals(name)) {
+ int formalParamCount = method.getParameterTypes().length;
+ if (method.isVarArgs() && paramCount >= formalParamCount - 1) {
+ varArgsMethod = method;
+ } else if (paramCount == formalParamCount) {
+ return findAccessibleMethod(method);
+ }
+ }
+ }
+ return varArgsMethod == null ? null : findAccessibleMethod(varArgsMethod);
+ }
+
+ /**
+ * Lookup an expression factory used to coerce method parameters in context under key
+ * "javax.el.ExpressionFactory".
+ * If no expression factory can be found under that key, use a default instance created with
+ * {@link ExpressionFactory#newInstance()}.
+ * @param context
+ * The context of this evaluation.
+ * @return expression factory instance
+ */
+ protected ExpressionFactory getExpressionFactory(ELContext context) {
+ Object obj = context.getContext(ExpressionFactory.class);
+ if (obj instanceof ExpressionFactory) {
+ return (ExpressionFactory) obj;
+ }
+ if (defaultFactory == null) {
+ defaultFactory = ExpressionFactory.newInstance();
+ }
+ return defaultFactory;
+ }
+
+ protected Object[] coerceParams(
+ ExpressionFactory factory,
+ Method method,
+ Object[] params
+ ) {
+ Class>[] types = method.getParameterTypes();
+ Object[] args = new Object[types.length];
+ if (method.isVarArgs()) {
+ int varargIndex = types.length - 1;
+ if (params.length < varargIndex) {
+ throw new ELException("Bad argument count");
+ }
+ for (int i = 0; i < varargIndex; i++) {
+ coerceValue(args, i, factory, params[i], types[i]);
+ }
+ Class> varargType = types[varargIndex].getComponentType();
+ int length = params.length - varargIndex;
+ Object array = null;
+ if (length == 1) {
+ Object source = params[varargIndex];
+ if (source != null && source.getClass().isArray()) {
+ if (types[varargIndex].isInstance(source)) { // use source array as is
+ array = source;
+ } else { // coerce array elements
+ length = Array.getLength(source);
+ array = Array.newInstance(varargType, length);
+ for (int i = 0; i < length; i++) {
+ coerceValue(array, i, factory, Array.get(source, i), varargType);
+ }
+ }
+ } else { // single element array
+ array = Array.newInstance(varargType, 1);
+ coerceValue(array, 0, factory, source, varargType);
+ }
+ } else {
+ array = Array.newInstance(varargType, length);
+ for (int i = 0; i < length; i++) {
+ coerceValue(array, i, factory, params[varargIndex + i], varargType);
+ }
+ }
+ args[varargIndex] = array;
+ } else {
+ if (params.length != args.length) {
+ throw new ELException("Bad argument count");
+ }
+ for (int i = 0; i < args.length; i++) {
+ coerceValue(args, i, factory, params[i], types[i]);
+ }
+ }
+ return args;
+ }
+
+ protected Method getWriteMethod(Object base, Object property) {
+ return toBeanProperty(base, property).getWriteMethod();
+ }
+
+ protected Method getReadMethod(Object base, Object property) {
+ return toBeanProperty(base, property).getReadMethod();
+ }
+
+ protected void coerceValue(
+ Object array,
+ int index,
+ ExpressionFactory factory,
+ Object value,
+ Class> type
+ ) {
+ if (value != null || type.isPrimitive()) {
+ Array.set(array, index, factory.coerceToType(value, type));
+ }
+ }
+
+ /**
+ * Test whether the given base should be resolved by this ELResolver.
+ *
+ * @param base
+ * The bean to analyze.
+ * @return base != null
+ */
+ private boolean isResolvable(Object base) {
+ return base != null;
+ }
+
+ /**
+ * Lookup BeanProperty for the given (base, property) pair.
+ *
+ * @param base
+ * The bean to analyze.
+ * @param property
+ * The name of the property to analyze. Will be coerced to a String.
+ * @return The BeanProperty representing (base, property).
+ * @throws PropertyNotFoundException
+ * if no BeanProperty can be found.
+ */
+ private BeanProperty toBeanProperty(Object base, Object property) {
+ BeanProperties beanProperties = cache.get(base.getClass());
+ if (beanProperties == null) {
+ BeanProperties newBeanProperties = new BeanProperties(base.getClass());
+ beanProperties = cache.putIfAbsent(base.getClass(), newBeanProperties);
+ if (beanProperties == null) { // put succeeded, use new value
+ beanProperties = newBeanProperties;
+ }
+ }
+ BeanProperty beanProperty = property == null
+ ? null
+ : beanProperties.getBeanProperty(property.toString());
+ if (beanProperty == null) {
+ throw propertyNotFoundException;
+ }
+ return beanProperty;
+ }
+
+ /**
+ * This method is not part of the API, though it can be used (reflectively) by clients of this
+ * class to remove entries from the cache when the beans are being unloaded.
+ *
+ * Note: this method is present in the reference implementation, so we're adding it here to ease
+ * migration.
+ *
+ * @param loader
+ * The classLoader used to load the beans.
+ */
+ @SuppressWarnings("unused")
+ private void purgeBeanClasses(ClassLoader loader) {
+ Iterator> classes = cache.keySet().iterator();
+ while (classes.hasNext()) {
+ if (loader == classes.next().getClassLoader()) {
+ classes.remove();
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/CollectionMembershipOperator.java b/src/main/java/com/hubspot/jinjava/el/ext/CollectionMembershipOperator.java
index 28c93dd0a..662197cfe 100644
--- a/src/main/java/com/hubspot/jinjava/el/ext/CollectionMembershipOperator.java
+++ b/src/main/java/com/hubspot/jinjava/el/ext/CollectionMembershipOperator.java
@@ -9,6 +9,7 @@
import de.odysseus.el.tree.impl.ast.AstBinary.SimpleOperator;
import de.odysseus.el.tree.impl.ast.AstNode;
import java.util.Collection;
+import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
@@ -47,14 +48,30 @@ public Object apply(TypeConverter converter, Object o1, Object o2) {
if (Map.class.isAssignableFrom(o2.getClass())) {
Map map = (Map) o2;
- if (!map.isEmpty()) {
- try {
- Class> keyClass = map.keySet().iterator().next().getClass();
- return map.containsKey(converter.convert(o1, keyClass));
- } catch (ELException | NoSuchElementException e) {
- return Boolean.FALSE;
+ // An implementation of Map can override isEmpty() to false, but return empty keySet.
+ if (map.isEmpty() || map.keySet().isEmpty()) {
+ return Boolean.FALSE;
+ }
+ Iterator iterator = map.keySet().iterator();
+ Object key = iterator.next();
+ if (key == null) {
+ if (o1 == null) {
+ return Boolean.TRUE;
+ } else {
+ if (iterator.hasNext()) {
+ // Must be non-null this time.
+ key = iterator.next();
+ } else {
+ return Boolean.FALSE;
+ }
}
}
+ try {
+ Class> keyClass = key.getClass();
+ return map.containsKey(converter.convert(o1, keyClass));
+ } catch (ELException | NoSuchElementException e) {
+ return Boolean.FALSE;
+ }
}
return Boolean.FALSE;
@@ -65,7 +82,8 @@ public String toString() {
return TOKEN.getImage();
}
- public static final CollectionMembershipOperator OP = new CollectionMembershipOperator();
+ public static final CollectionMembershipOperator OP =
+ new CollectionMembershipOperator();
public static final Scanner.ExtensionToken TOKEN = new Scanner.ExtensionToken("in");
public static final ExtensionHandler HANDLER = getHandler(false);
@@ -73,7 +91,6 @@ public String toString() {
private static ExtensionHandler getHandler(boolean eager) {
return new ExtensionHandler(ExtensionPoint.CMP) {
-
@Override
public AstNode createAstNode(AstNode... children) {
return eager
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/CollectionNonMembershipOperator.java b/src/main/java/com/hubspot/jinjava/el/ext/CollectionNonMembershipOperator.java
index dc1e6b4f5..cd7bb681f 100644
--- a/src/main/java/com/hubspot/jinjava/el/ext/CollectionNonMembershipOperator.java
+++ b/src/main/java/com/hubspot/jinjava/el/ext/CollectionNonMembershipOperator.java
@@ -21,8 +21,10 @@ public String toString() {
return TOKEN.getImage();
}
- public static final CollectionNonMembershipOperator NOT_IN_OP = new CollectionNonMembershipOperator();
- public static final CollectionMembershipOperator IN_OP = new CollectionMembershipOperator();
+ public static final CollectionNonMembershipOperator NOT_IN_OP =
+ new CollectionNonMembershipOperator();
+ public static final CollectionMembershipOperator IN_OP =
+ new CollectionMembershipOperator();
public static final Scanner.ExtensionToken TOKEN = new Scanner.ExtensionToken("not in");
public static final ExtensionHandler HANDLER = getHandler(false);
@@ -30,7 +32,6 @@ public String toString() {
private static ExtensionHandler getHandler(boolean eager) {
return new ExtensionHandler(ExtensionPoint.CMP) {
-
@Override
public AstNode createAstNode(AstNode... children) {
return eager
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/DeferredInvocationResolutionException.java b/src/main/java/com/hubspot/jinjava/el/ext/DeferredInvocationResolutionException.java
new file mode 100644
index 000000000..4a31f6277
--- /dev/null
+++ b/src/main/java/com/hubspot/jinjava/el/ext/DeferredInvocationResolutionException.java
@@ -0,0 +1,8 @@
+package com.hubspot.jinjava.el.ext;
+
+public class DeferredInvocationResolutionException extends DeferredParsingException {
+
+ public DeferredInvocationResolutionException(String invocationResultString) {
+ super(DeferredInvocationResolutionException.class, invocationResultString);
+ }
+}
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/DeferredParsingException.java b/src/main/java/com/hubspot/jinjava/el/ext/DeferredParsingException.java
index c8842308e..b03d1e6e0 100644
--- a/src/main/java/com/hubspot/jinjava/el/ext/DeferredParsingException.java
+++ b/src/main/java/com/hubspot/jinjava/el/ext/DeferredParsingException.java
@@ -3,16 +3,29 @@
import com.hubspot.jinjava.interpret.DeferredValueException;
public class DeferredParsingException extends DeferredValueException {
+
private final String deferredEvalResult;
private final Object sourceNode;
+ private final IdentifierPreservationStrategy identifierPreservationStrategy;
- public DeferredParsingException(String message) {
- super(message);
- this.deferredEvalResult = message;
- this.sourceNode = null;
+ public DeferredParsingException(Object sourceNode, String deferredEvalResult) {
+ super(
+ String.format(
+ "%s could not be parsed more than: %s",
+ sourceNode.getClass(),
+ deferredEvalResult
+ )
+ );
+ this.deferredEvalResult = deferredEvalResult;
+ this.sourceNode = sourceNode;
+ this.identifierPreservationStrategy = IdentifierPreservationStrategy.RESOLVING;
}
- public DeferredParsingException(Object sourceNode, String deferredEvalResult) {
+ public DeferredParsingException(
+ Object sourceNode,
+ String deferredEvalResult,
+ IdentifierPreservationStrategy identifierPreservationStrategy
+ ) {
super(
String.format(
"%s could not be parsed more than: %s",
@@ -22,6 +35,7 @@ public DeferredParsingException(Object sourceNode, String deferredEvalResult) {
);
this.deferredEvalResult = deferredEvalResult;
this.sourceNode = sourceNode;
+ this.identifierPreservationStrategy = identifierPreservationStrategy;
}
public String getDeferredEvalResult() {
@@ -31,4 +45,8 @@ public String getDeferredEvalResult() {
public Object getSourceNode() {
return sourceNode;
}
+
+ public IdentifierPreservationStrategy getIdentifierPreservationStrategy() {
+ return identifierPreservationStrategy;
+ }
}
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/ExtendedParser.java b/src/main/java/com/hubspot/jinjava/el/ext/ExtendedParser.java
index fb26213c0..a743aa2e0 100644
--- a/src/main/java/com/hubspot/jinjava/el/ext/ExtendedParser.java
+++ b/src/main/java/com/hubspot/jinjava/el/ext/ExtendedParser.java
@@ -51,6 +51,7 @@
import javax.el.ELException;
public class ExtendedParser extends Parser {
+
public static final String INTERPRETER = "____int3rpr3t3r____";
public static final String FILTER_PREFIX = "filter:";
public static final String EXPTEST_PREFIX = "exptest:";
@@ -116,7 +117,6 @@ public ExtendedParser(Builder context, String input) {
putExtensionHandler(
PIPE,
new ExtensionHandler(ExtensionPoint.AND) {
-
@Override
public AstNode createAstNode(AstNode... children) {
throw new ELException("Illegal use of '|' operator");
@@ -281,14 +281,16 @@ protected AstNode nonliteral() throws ScanException, ParseException {
switch (getToken().getSymbol()) {
case IDENTIFIER:
String name = consumeToken().getImage();
- if (
- getToken().getSymbol() == COLON &&
- lookahead(0).getSymbol() == IDENTIFIER &&
- (lookahead(1).getSymbol() == LPAREN || (isPossibleExpTestOrFilter(name)))
- ) { // ns:f(...)
- consumeToken();
- name += ":" + getToken().getImage();
- consumeToken();
+ if (getToken().getSymbol() == COLON && getToken().getImage().equals(":")) {
+ Symbol lookahead = lookahead(0).getSymbol();
+ if (
+ isPossibleExpTest(lookahead) &&
+ (lookahead(1).getSymbol() == LPAREN || (isPossibleExpTestOrFilter(name)))
+ ) { // ns:f(...)
+ consumeToken();
+ name += ":" + getToken().getImage();
+ consumeToken();
+ }
}
if (getToken().getSymbol() == LPAREN) { // function
v = function(name, params());
@@ -529,30 +531,11 @@ protected AstNode value() throws ScanException, ParseException {
private AstNode parseOperators(AstNode left) throws ScanException, ParseException {
if ("|".equals(getToken().getImage()) && lookahead(0).getSymbol() == IDENTIFIER) {
- AstNode v = left;
-
- do {
- consumeToken(); // '|'
- String filterName = consumeToken().getImage();
- List filterParams = Lists.newArrayList(v, interpreter());
-
- // optional filter args
- if (getToken().getSymbol() == Symbol.LPAREN) {
- AstParameters astParameters = params();
- for (int i = 0; i < astParameters.getCardinality(); i++) {
- filterParams.add(astParameters.getChild(i));
- }
- }
-
- AstProperty filterProperty = createAstDot(
- identifier(FILTER_PREFIX + filterName),
- "filter",
- true
- );
- v = createAstMethod(filterProperty, createAstParameters(filterParams)); // function("filter:" + filterName, new AstParameters(filterParams));
- } while ("|".equals(getToken().getImage()));
-
- return v;
+ if (shouldUseFilterChainOptimization()) {
+ return parseFiltersAsChain(left);
+ } else {
+ return parseFiltersAsNestedMethods(left);
+ }
} else if (
"is".equals(getToken().getImage()) &&
"not".equals(lookahead(0).getImage()) &&
@@ -575,6 +558,68 @@ protected AstParameters createAstParameters(List nodes) {
return new AstParameters(nodes);
}
+ protected AstFilterChain createAstFilterChain(
+ AstNode input,
+ List filterSpecs
+ ) {
+ return new AstFilterChain(input, filterSpecs);
+ }
+
+ private AstNode parseFiltersAsChain(AstNode left) throws ScanException, ParseException {
+ List filterSpecs = new ArrayList<>();
+
+ do {
+ consumeToken(); // '|'
+ String filterName = consumeToken().getImage();
+ AstParameters filterParams = null;
+
+ // optional filter args
+ if (getToken().getSymbol() == Symbol.LPAREN) {
+ filterParams = params();
+ }
+
+ filterSpecs.add(new FilterSpec(filterName, filterParams));
+ } while ("|".equals(getToken().getImage()));
+
+ return createAstFilterChain(left, filterSpecs);
+ }
+
+ protected AstNode parseFiltersAsNestedMethods(AstNode left)
+ throws ScanException, ParseException {
+ AstNode v = left;
+
+ do {
+ consumeToken(); // '|'
+ String filterName = consumeToken().getImage();
+ List filterParams = Lists.newArrayList(v, interpreter());
+
+ // optional filter args
+ if (getToken().getSymbol() == Symbol.LPAREN) {
+ AstParameters astParameters = params();
+ for (int i = 0; i < astParameters.getCardinality(); i++) {
+ filterParams.add(astParameters.getChild(i));
+ }
+ }
+
+ AstProperty filterProperty = createAstDot(
+ identifier(FILTER_PREFIX + filterName),
+ "filter",
+ true
+ );
+ v = createAstMethod(filterProperty, createAstParameters(filterParams));
+ } while ("|".equals(getToken().getImage()));
+
+ return v;
+ }
+
+ protected boolean shouldUseFilterChainOptimization() {
+ return JinjavaInterpreter
+ .getCurrentMaybe()
+ .map(JinjavaInterpreter::getConfig)
+ .map(JinjavaConfig::isEnableFilterChainOptimization)
+ .orElse(false);
+ }
+
private boolean isPossibleExpTest(Symbol symbol) {
return VALID_SYMBOLS_FOR_EXP_TEST.contains(symbol);
}
@@ -583,9 +628,9 @@ private boolean isPossibleExpTestOrFilter(String namespace)
throws ParseException, ScanException {
if (
FILTER_PREFIX.substring(0, FILTER_PREFIX.length() - 1).equals(namespace) ||
- EXPTEST_PREFIX.substring(0, EXPTEST_PREFIX.length() - 1).equals(namespace) &&
- lookahead(1).getSymbol() == DOT &&
- lookahead(2).getSymbol() == IDENTIFIER
+ (EXPTEST_PREFIX.substring(0, EXPTEST_PREFIX.length() - 1).equals(namespace) &&
+ lookahead(1).getSymbol() == DOT &&
+ lookahead(2).getSymbol() == IDENTIFIER)
) {
Token property = lookahead(2);
if (
@@ -624,7 +669,6 @@ protected Scanner createScanner(String expression) {
}
private static final ExtensionHandler NULL_EXT_HANDLER = new ExtensionHandler(null) {
-
@Override
public AstNode createAstNode(AstNode... children) {
return null;
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/ExtendedScanner.java b/src/main/java/com/hubspot/jinjava/el/ext/ExtendedScanner.java
index bf54da80f..1116c275b 100644
--- a/src/main/java/com/hubspot/jinjava/el/ext/ExtendedScanner.java
+++ b/src/main/java/com/hubspot/jinjava/el/ext/ExtendedScanner.java
@@ -1,6 +1,7 @@
package com.hubspot.jinjava.el.ext;
import de.odysseus.el.tree.impl.Scanner;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -58,6 +59,10 @@ protected boolean isWhitespace(char c) {
}
}
+ @SuppressFBWarnings(
+ value = "HSM_HIDING_METHOD",
+ justification = "Purposefully overriding to use static method instance of this class."
+ )
protected static void addKeyToken(Token token) {
try {
ADD_KEY_TOKEN_METHOD.invoke(null, token);
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/FilterSpec.java b/src/main/java/com/hubspot/jinjava/el/ext/FilterSpec.java
new file mode 100644
index 000000000..175016913
--- /dev/null
+++ b/src/main/java/com/hubspot/jinjava/el/ext/FilterSpec.java
@@ -0,0 +1,48 @@
+package com.hubspot.jinjava.el.ext;
+
+import de.odysseus.el.tree.impl.ast.AstParameters;
+import java.util.Objects;
+
+/**
+ * Specification for a filter in a filter chain.
+ * Holds the filter name and optional parameters.
+ */
+public class FilterSpec {
+
+ private final String name;
+ private final AstParameters params;
+
+ public FilterSpec(String name, AstParameters params) {
+ this.name = Objects.requireNonNull(name, "Filter name cannot be null");
+ this.params = params;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public AstParameters getParams() {
+ return params;
+ }
+
+ public boolean hasParams() {
+ return params != null && params.getCardinality() > 0;
+ }
+
+ @Override
+ public String toString() {
+ if (hasParams()) {
+ StringBuilder sb = new StringBuilder(name);
+ sb.append('(');
+ for (int i = 0; i < params.getCardinality(); i++) {
+ if (i > 0) {
+ sb.append(", ");
+ }
+ sb.append(params.getChild(i));
+ }
+ sb.append(')');
+ return sb.toString();
+ }
+ return name;
+ }
+}
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/IdentifierPreservationStrategy.java b/src/main/java/com/hubspot/jinjava/el/ext/IdentifierPreservationStrategy.java
new file mode 100644
index 000000000..d3e6536dd
--- /dev/null
+++ b/src/main/java/com/hubspot/jinjava/el/ext/IdentifierPreservationStrategy.java
@@ -0,0 +1,20 @@
+package com.hubspot.jinjava.el.ext;
+
+public enum IdentifierPreservationStrategy {
+ PRESERVING(true),
+ RESOLVING(false);
+
+ public static IdentifierPreservationStrategy preserving(boolean preserveIdentifier) {
+ return preserveIdentifier ? PRESERVING : RESOLVING;
+ }
+
+ private final boolean preserving;
+
+ IdentifierPreservationStrategy(boolean preserving) {
+ this.preserving = preserving;
+ }
+
+ public boolean isPreserving() {
+ return preserving;
+ }
+}
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolver.java b/src/main/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolver.java
index 30549acc9..dad811583 100644
--- a/src/main/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolver.java
+++ b/src/main/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolver.java
@@ -2,68 +2,133 @@
import com.google.common.base.CaseFormat;
import com.google.common.collect.ImmutableSet;
+import com.hubspot.jinjava.interpret.DeferredValueException;
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
-import java.lang.reflect.Constructor;
-import java.lang.reflect.Field;
+import com.hubspot.jinjava.util.EagerReconstructionUtils;
+import java.beans.IntrospectionException;
+import java.beans.Introspector;
+import java.beans.MethodDescriptor;
+import java.lang.invoke.MethodType;
+import java.lang.reflect.Array;
import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
import java.util.Set;
-import javax.el.BeanELResolver;
+import java.util.concurrent.ConcurrentHashMap;
import javax.el.ELContext;
+import javax.el.ELException;
+import javax.el.ExpressionFactory;
import javax.el.MethodNotFoundException;
/**
* {@link BeanELResolver} supporting snake case property names.
*/
public class JinjavaBeanELResolver extends BeanELResolver {
- private static final Set RESTRICTED_PROPERTIES = ImmutableSet
- .builder()
- .add("class")
- .build();
- private static final Set RESTRICTED_METHODS = ImmutableSet
+ private static final Set DEFERRED_EXECUTION_RESTRICTED_METHODS = ImmutableSet
.builder()
- .add("class")
- .add("clone")
- .add("hashCode")
- .add("getClass")
- .add("getDeclaringClass")
- .add("forName")
- .add("notify")
- .add("notifyAll")
- .add("wait")
+ .add("put")
+ .add("putAll")
+ .add("update")
+ .add("add")
+ .add("insert")
+ .add("pop")
+ .add("append")
+ .add("extend")
+ .add("clear")
+ .add("remove")
+ .add("addAll")
+ .add("removeAll")
+ .add("replace")
+ .add("replaceAll")
+ .add("putIfAbsent")
+ .add("sort")
+ .add("set")
+ .add("merge")
.build();
- /**
- * Creates a new read/write {@link JinjavaBeanELResolver}.
- */
- public JinjavaBeanELResolver() {}
+ protected static final class BeanMethods {
+
+ private final Map> map = new HashMap<>();
+
+ public BeanMethods(Class> baseClass) {
+ MethodDescriptor[] descriptors;
+ try {
+ descriptors = Introspector.getBeanInfo(baseClass).getMethodDescriptors();
+ } catch (IntrospectionException e) {
+ throw new ELException(e);
+ }
+ for (MethodDescriptor descriptor : descriptors) {
+ map.compute(
+ descriptor.getName(),
+ (k, v) -> {
+ if (v == null) {
+ v = new LinkedList<>();
+ }
+ v.add(new BeanMethod(descriptor));
+ return v;
+ }
+ );
+ }
+ }
+
+ public List getBeanMethods(String methodName) {
+ return map.get(methodName);
+ }
+
+ protected static final class BeanMethod {
+
+ private final MethodDescriptor descriptor;
+
+ private volatile Method method;
+
+ public BeanMethod(MethodDescriptor descriptor) {
+ this.descriptor = descriptor;
+ }
+
+ public Method getMethod() {
+ if (method == null) {
+ method = findAccessibleMethod(descriptor.getMethod());
+ }
+ return method;
+ }
+ }
+ }
+
+ private final ConcurrentHashMap, BeanMethods> beanMethodsCache;
+
+ public JinjavaBeanELResolver() {
+ this(true);
+ }
/**
- * Creates a new {@link JinjavaBeanELResolver} whose read-only status is determined by the given parameter.
+ * Creates a new read/write {@link JinjavaBeanELResolver}.
*/
public JinjavaBeanELResolver(boolean readOnly) {
super(readOnly);
+ this.beanMethodsCache = new ConcurrentHashMap, BeanMethods>();
}
@Override
public Class> getType(ELContext context, Object base, Object property) {
- return super.getType(context, base, validatePropertyName(property));
+ return super.getType(context, base, transformPropertyName(property));
}
@Override
public Object getValue(ELContext context, Object base, Object property) {
- Object result = super.getValue(context, base, validatePropertyName(property));
- return result instanceof Class ? null : result;
+ return super.getValue(context, base, transformPropertyName(property));
}
@Override
public boolean isReadOnly(ELContext context, Object base, Object property) {
- return super.isReadOnly(context, base, validatePropertyName(property));
+ return super.isReadOnly(context, base, transformPropertyName(property));
}
@Override
public void setValue(ELContext context, Object base, Object property, Object value) {
- super.setValue(context, base, validatePropertyName(property), value);
+ super.setValue(context, base, transformPropertyName(property), value);
}
@Override
@@ -74,37 +139,134 @@ public Object invoke(
Class>[] paramTypes,
Object[] params
) {
- if (method == null || RESTRICTED_METHODS.contains(method.toString())) {
- throw new MethodNotFoundException(
- "Cannot find method '" + method + "' in " + base.getClass()
+ if (method == null) {
+ throw new MethodNotFoundException("Cannot find method null in " + base.getClass());
+ }
+ if (
+ DEFERRED_EXECUTION_RESTRICTED_METHODS.contains(method.toString()) &&
+ EagerReconstructionUtils.isDeferredExecutionMode()
+ ) {
+ throw new DeferredValueException(
+ String.format(
+ "Cannot run method '%s' in %s in deferred execution mode",
+ method,
+ base.getClass()
+ )
);
}
- if (isRestrictedClass(base)) {
- throw new MethodNotFoundException(
- "Cannot find method '" + method + "' in " + base.getClass()
- );
+ return super.invoke(context, base, method, paramTypes, params);
+ }
+
+ // As opposed to supporting ____int3rpr3t3r____, coerce JinjavaInterpreter on validated methods
+ @Override
+ protected void coerceValue(
+ Object array,
+ int index,
+ ExpressionFactory factory,
+ Object value,
+ Class> type
+ ) {
+ if (type.equals(JinjavaInterpreter.class)) {
+ Array.set(array, index, JinjavaInterpreter.getCurrent()); // Could be null if there's no current interpreter
+ } else {
+ super.coerceValue(array, index, factory, value, type);
}
+ }
+
+ @Override
+ protected Method findMethod(
+ Object base,
+ String name,
+ Class>[] types,
+ Object[] params,
+ int paramCount
+ ) {
+ Method method;
+ if (types != null) {
+ method = super.findMethod(base, name, types, params, paramCount);
+ } else {
+ Method varArgsMethod = null;
+ BeanMethods beanMethods = beanMethodsCache.get(base.getClass());
+ if (beanMethods == null) {
+ BeanMethods newBeanMethods = new BeanMethods(base.getClass());
+ beanMethods = beanMethodsCache.putIfAbsent(base.getClass(), newBeanMethods);
+ if (beanMethods == null) { // put succeeded, use new value
+ beanMethods = newBeanMethods;
+ }
+ }
- Object result = super.invoke(context, base, method, paramTypes, params);
+ List potentialMethods = new LinkedList<>();
- if (isRestrictedClass(result)) {
- throw new MethodNotFoundException(
- "Cannot find method '" + method + "' in " + base.getClass()
- );
+ List methodsForName = beanMethods.getBeanMethods(name);
+ if (methodsForName == null) {
+ methodsForName = List.of();
+ }
+ for (BeanMethods.BeanMethod bm : methodsForName) {
+ Method m = bm.getMethod();
+ int formalParamCount = m.getParameterTypes().length;
+ if (m.isVarArgs() && paramCount >= formalParamCount - 1) {
+ varArgsMethod = m;
+ } else if (paramCount == formalParamCount) {
+ potentialMethods.add(m);
+ }
+ }
+ final Method finalVarArgsMethod = varArgsMethod;
+ method =
+ potentialMethods
+ .stream()
+ .filter(m -> checkAssignableParameterTypes(params, m))
+ .min(JinjavaBeanELResolver::pickMoreSpecificMethod)
+ .orElseGet(() -> potentialMethods.stream().findAny().orElse(finalVarArgsMethod)
+ );
}
+ return getAllowlistMethodValidator().validateMethod(method);
+ }
- return result;
+ @Override
+ protected Method getWriteMethod(Object base, Object property) {
+ return getAllowlistMethodValidator()
+ .validateMethod(super.getWriteMethod(base, property));
}
- private String validatePropertyName(Object property) {
- String propertyName = transformPropertyName(property);
+ @Override
+ protected Method getReadMethod(Object base, Object property) {
+ return getAllowlistMethodValidator()
+ .validateMethod(super.getReadMethod(base, property));
+ }
- if (RESTRICTED_PROPERTIES.contains(propertyName)) {
- return null;
+ private static AllowlistMethodValidator getAllowlistMethodValidator() {
+ return JinjavaInterpreter
+ .getCurrentMaybe()
+ .map(interpreter -> interpreter.getConfig().getMethodValidator())
+ .orElse(AllowlistMethodValidator.DEFAULT);
+ }
+
+ private static boolean checkAssignableParameterTypes(Object[] params, Method method) {
+ for (int i = 0; i < method.getParameterTypes().length; i++) {
+ Class> paramType = method.getParameterTypes()[i];
+ if (paramType.isPrimitive()) {
+ paramType = MethodType.methodType(paramType).wrap().returnType();
+ }
+ if (params[i] != null && !paramType.isAssignableFrom(params[i].getClass())) {
+ return false;
+ }
}
+ return true;
+ }
- return propertyName;
+ private static int pickMoreSpecificMethod(Method methodA, Method methodB) {
+ Class>[] typesA = methodA.getParameterTypes();
+ Class>[] typesB = methodB.getParameterTypes();
+ for (int i = 0; i < typesA.length; i++) {
+ if (!typesA[i].isAssignableFrom(typesB[i])) {
+ if (typesB[i].isPrimitive()) {
+ return 1;
+ }
+ return -1;
+ }
+ }
+ return 1;
}
/**
@@ -121,24 +283,4 @@ private String transformPropertyName(Object property) {
}
return CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, propertyStr);
}
-
- protected boolean isRestrictedClass(Object o) {
- if (o == null) {
- return false;
- }
-
- return (
- (
- o.getClass().getPackage() != null &&
- o.getClass().getPackage().getName().startsWith("java.lang.reflect")
- ) ||
- o instanceof Class ||
- o instanceof ClassLoader ||
- o instanceof Thread ||
- o instanceof Method ||
- o instanceof Field ||
- o instanceof Constructor ||
- o instanceof JinjavaInterpreter
- );
- }
}
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/MethodValidator.java b/src/main/java/com/hubspot/jinjava/el/ext/MethodValidator.java
new file mode 100644
index 000000000..eb065b7d2
--- /dev/null
+++ b/src/main/java/com/hubspot/jinjava/el/ext/MethodValidator.java
@@ -0,0 +1,10 @@
+package com.hubspot.jinjava.el.ext;
+
+import java.lang.reflect.Method;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+public interface MethodValidator {
+ @Nullable
+ Method validateMethod(@Nonnull Method m);
+}
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/MethodValidatorConfig.java b/src/main/java/com/hubspot/jinjava/el/ext/MethodValidatorConfig.java
new file mode 100644
index 000000000..52ecea9e1
--- /dev/null
+++ b/src/main/java/com/hubspot/jinjava/el/ext/MethodValidatorConfig.java
@@ -0,0 +1,77 @@
+package com.hubspot.jinjava.el.ext;
+
+import com.google.common.collect.ImmutableSet;
+import com.hubspot.jinjava.JinjavaImmutableStyle;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import org.immutables.value.Value;
+
+@Value.Immutable(singleton = true)
+@JinjavaImmutableStyle
+public abstract class MethodValidatorConfig {
+
+ public abstract ImmutableSet allowedMethods();
+
+ public abstract ImmutableSet allowedDeclaredMethodsFromCanonicalClassPrefixes();
+
+ public abstract ImmutableSet allowedDeclaredMethodsFromCanonicalClassNames();
+
+ @Value.Default
+ public Consumer onRejectedMethod() {
+ return m -> {};
+ }
+
+ @Value.Check
+ void banClassesAndMethods() {
+ List list = BannedAllowlistOptions.findBannedPrefixes(
+ Stream
+ .of(
+ allowedMethods()
+ .stream()
+ .map(method -> method.getDeclaringClass().getCanonicalName()),
+ allowedDeclaredMethodsFromCanonicalClassPrefixes().stream(),
+ allowedDeclaredMethodsFromCanonicalClassNames().stream()
+ )
+ .flatMap(Function.identity())
+ );
+ if (!list.isEmpty()) {
+ throw new IllegalStateException(
+ "Banned classes or prefixes (Object.class, Class.class, java.lang.reflect, com.fasterxml.jackson.databind) are not allowed: " +
+ list
+ );
+ }
+ }
+
+ public static MethodValidatorConfig of() {
+ return ImmutableMethodValidatorConfig.of();
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder extends ImmutableMethodValidatorConfig.Builder {
+
+ Builder() {}
+
+ public Builder addDefaultAllowlistGroups() {
+ return addAllowlistGroups(AllowlistGroup.values());
+ }
+
+ public Builder addAllowlistGroups(AllowlistGroup... allowlistGroups) {
+ for (AllowlistGroup allowlistGroup : allowlistGroups) {
+ this.addAllowedMethods(allowlistGroup.allowMethods())
+ .addAllowedDeclaredMethodsFromCanonicalClassPrefixes(
+ allowlistGroup.allowedDeclaredMethodsFromCanonicalClassPrefixes()
+ )
+ .addAllowedDeclaredMethodsFromCanonicalClassNames(
+ allowlistGroup.allowedDeclaredMethodsFromClasses()
+ );
+ }
+ return this;
+ }
+ }
+}
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/NamedParameter.java b/src/main/java/com/hubspot/jinjava/el/ext/NamedParameter.java
index 39170e65e..b47ba1133 100644
--- a/src/main/java/com/hubspot/jinjava/el/ext/NamedParameter.java
+++ b/src/main/java/com/hubspot/jinjava/el/ext/NamedParameter.java
@@ -1,9 +1,11 @@
package com.hubspot.jinjava.el.ext;
import com.hubspot.jinjava.objects.serialization.PyishSerializable;
+import java.io.IOException;
import java.util.Objects;
public class NamedParameter implements PyishSerializable {
+
private final String name;
private final Object value;
@@ -26,7 +28,12 @@ public String toString() {
}
@Override
- public String toPyishString() {
- return name + "=" + PyishSerializable.writeValueAsString(value);
+ @SuppressWarnings("unchecked")
+ public T appendPyishString(T appendable)
+ throws IOException {
+ return (T) appendable
+ .append(name)
+ .append('=')
+ .append(PyishSerializable.writeValueAsString(value));
}
}
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/NamedParameterOperator.java b/src/main/java/com/hubspot/jinjava/el/ext/NamedParameterOperator.java
index 369995c80..3d50953de 100644
--- a/src/main/java/com/hubspot/jinjava/el/ext/NamedParameterOperator.java
+++ b/src/main/java/com/hubspot/jinjava/el/ext/NamedParameterOperator.java
@@ -9,13 +9,13 @@
import javax.el.ELException;
public class NamedParameterOperator {
+
public static final Scanner.ExtensionToken TOKEN = new Scanner.ExtensionToken("=");
public static final ExtensionHandler HANDLER = getHandler(false);
public static ExtensionHandler getHandler(boolean eager) {
return new ExtensionHandler(ExtensionPoint.ADD) {
-
@Override
public AstNode createAstNode(AstNode... children) {
if (!(children[0] instanceof AstIdentifier)) {
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/PowerOfOperator.java b/src/main/java/com/hubspot/jinjava/el/ext/PowerOfOperator.java
index 166a42cc8..1858e67c7 100644
--- a/src/main/java/com/hubspot/jinjava/el/ext/PowerOfOperator.java
+++ b/src/main/java/com/hubspot/jinjava/el/ext/PowerOfOperator.java
@@ -10,6 +10,7 @@
import de.odysseus.el.tree.impl.ast.AstNode;
public class PowerOfOperator extends SimpleOperator {
+
public static final Scanner.ExtensionToken TOKEN = new Scanner.ExtensionToken("**");
public static final PowerOfOperator OP = new PowerOfOperator();
@@ -50,7 +51,6 @@ public String toString() {
public static ExtensionHandler getHandler(boolean eager) {
return new ExtensionHandler(ExtensionPoint.MUL) {
-
@Override
public AstNode createAstNode(AstNode... children) {
return eager
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/ReturnTypeValidator.java b/src/main/java/com/hubspot/jinjava/el/ext/ReturnTypeValidator.java
new file mode 100644
index 000000000..f60f0fe2a
--- /dev/null
+++ b/src/main/java/com/hubspot/jinjava/el/ext/ReturnTypeValidator.java
@@ -0,0 +1,8 @@
+package com.hubspot.jinjava.el.ext;
+
+import javax.annotation.Nullable;
+
+public interface ReturnTypeValidator {
+ @Nullable
+ Object validateReturnType(Object o);
+}
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/ReturnTypeValidatorConfig.java b/src/main/java/com/hubspot/jinjava/el/ext/ReturnTypeValidatorConfig.java
new file mode 100644
index 000000000..8a885c519
--- /dev/null
+++ b/src/main/java/com/hubspot/jinjava/el/ext/ReturnTypeValidatorConfig.java
@@ -0,0 +1,76 @@
+package com.hubspot.jinjava.el.ext;
+
+import com.google.common.collect.ImmutableSet;
+import com.hubspot.jinjava.JinjavaImmutableStyle;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import org.immutables.value.Value;
+
+@Value.Immutable(singleton = true)
+@JinjavaImmutableStyle
+public abstract class ReturnTypeValidatorConfig {
+
+ public abstract ImmutableSet allowedCanonicalClassPrefixes();
+
+ public abstract ImmutableSet allowedCanonicalClassNames();
+
+ @Value.Default
+ public Consumer> onRejectedClass() {
+ return m -> {};
+ }
+
+ @Value.Default
+ public boolean allowArrays() {
+ return false;
+ }
+
+ @Value.Check
+ void banClassesAndMethods() {
+ List list = BannedAllowlistOptions.findBannedPrefixes(
+ Stream
+ .of(
+ allowedCanonicalClassPrefixes().stream(),
+ allowedCanonicalClassNames().stream()
+ )
+ .flatMap(Function.identity())
+ );
+ if (!list.isEmpty()) {
+ throw new IllegalStateException(
+ "Banned classes or prefixes (Object.class, Class.class, java.lang.reflect, com.fasterxml.jackson.databind) are not allowed: " +
+ list
+ );
+ }
+ }
+
+ public static ReturnTypeValidatorConfig of() {
+ return ImmutableReturnTypeValidatorConfig.of();
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder extends ImmutableReturnTypeValidatorConfig.Builder {
+
+ Builder() {}
+
+ public Builder addDefaultAllowlistGroups() {
+ return addAllowlistGroups(AllowlistGroup.values());
+ }
+
+ public Builder addAllowlistGroups(AllowlistGroup... allowlistGroups) {
+ for (AllowlistGroup allowlistGroup : allowlistGroups) {
+ this.addAllowedCanonicalClassPrefixes(
+ allowlistGroup.allowedReturnTypeCanonicalClassPrefixes()
+ )
+ .addAllowedCanonicalClassNames(allowlistGroup.allowedReturnTypeClasses());
+ if (allowlistGroup.enableArrays()) {
+ this.setAllowArrays(true);
+ }
+ }
+ return this;
+ }
+ }
+}
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/StringBuildingOperator.java b/src/main/java/com/hubspot/jinjava/el/ext/StringBuildingOperator.java
new file mode 100644
index 000000000..47296089f
--- /dev/null
+++ b/src/main/java/com/hubspot/jinjava/el/ext/StringBuildingOperator.java
@@ -0,0 +1,16 @@
+package com.hubspot.jinjava.el.ext;
+
+import com.hubspot.jinjava.interpret.JinjavaInterpreter;
+import com.hubspot.jinjava.util.LengthLimitingStringBuilder;
+
+public interface StringBuildingOperator {
+ default LengthLimitingStringBuilder getStringBuilder() {
+ JinjavaInterpreter interpreter = JinjavaInterpreter.getCurrent();
+
+ long maxSize = (interpreter == null || interpreter.getConfig() == null)
+ ? 0
+ : interpreter.getConfig().getMaxOutputSize();
+
+ return new LengthLimitingStringBuilder(maxSize);
+ }
+}
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/StringConcatOperator.java b/src/main/java/com/hubspot/jinjava/el/ext/StringConcatOperator.java
index d00073eee..16ddc66bc 100644
--- a/src/main/java/com/hubspot/jinjava/el/ext/StringConcatOperator.java
+++ b/src/main/java/com/hubspot/jinjava/el/ext/StringConcatOperator.java
@@ -9,14 +9,16 @@
import de.odysseus.el.tree.impl.ast.AstBinary.SimpleOperator;
import de.odysseus.el.tree.impl.ast.AstNode;
-public class StringConcatOperator extends SimpleOperator {
+public class StringConcatOperator
+ extends SimpleOperator
+ implements StringBuildingOperator {
@Override
protected Object apply(TypeConverter converter, Object o1, Object o2) {
String o1s = converter.convert(o1, String.class);
String o2s = converter.convert(o2, String.class);
- return new StringBuilder(o1s).append(o2s).toString();
+ return getStringBuilder().append(o1s).append(o2s).toString();
}
@Override
@@ -31,7 +33,6 @@ public String toString() {
public static ExtensionHandler getHandler(boolean eager) {
return new ExtensionHandler(ExtensionPoint.ADD) {
-
@Override
public AstNode createAstNode(AstNode... children) {
return eager
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/TruncDivOperator.java b/src/main/java/com/hubspot/jinjava/el/ext/TruncDivOperator.java
index d8b4fd146..b28e4d0a1 100644
--- a/src/main/java/com/hubspot/jinjava/el/ext/TruncDivOperator.java
+++ b/src/main/java/com/hubspot/jinjava/el/ext/TruncDivOperator.java
@@ -10,6 +10,7 @@
import de.odysseus.el.tree.impl.ast.AstNode;
public class TruncDivOperator extends SimpleOperator {
+
public static final Scanner.ExtensionToken TOKEN = new Scanner.ExtensionToken("//");
public static final TruncDivOperator OP = new TruncDivOperator();
@@ -50,7 +51,6 @@ public String toString() {
public static ExtensionHandler getHandler(boolean eager) {
return new ExtensionHandler(ExtensionPoint.MUL) {
-
@Override
public AstNode createAstNode(AstNode... children) {
return eager
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstBinary.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstBinary.java
index 95d92e917..ea8c27ef1 100644
--- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstBinary.java
+++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstBinary.java
@@ -2,6 +2,7 @@
import com.hubspot.jinjava.el.NoInvokeELContext;
import com.hubspot.jinjava.el.ext.DeferredParsingException;
+import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy;
import com.hubspot.jinjava.el.ext.OrOperator;
import de.odysseus.el.tree.Bindings;
import de.odysseus.el.tree.impl.ast.AstBinary;
@@ -9,6 +10,7 @@
import javax.el.ELContext;
public class EagerAstBinary extends AstBinary implements EvalResultHolder {
+
protected Object evalResult;
protected boolean hasEvalResult;
protected final EvalResultHolder left;
@@ -48,7 +50,7 @@ public String getPartiallyResolved(
Bindings bindings,
ELContext context,
DeferredParsingException deferredParsingException,
- boolean preserveIdentifier
+ IdentifierPreservationStrategy identifierPreservationStrategy
) {
return (
EvalResultHolder.reconstructNode(
@@ -56,7 +58,7 @@ public String getPartiallyResolved(
context,
left,
deferredParsingException,
- false
+ IdentifierPreservationStrategy.RESOLVING
) +
String.format(" %s ", operator.toString()) +
EvalResultHolder.reconstructNode(
@@ -66,7 +68,7 @@ public String getPartiallyResolved(
: context,
right,
deferredParsingException,
- false
+ IdentifierPreservationStrategy.RESOLVING
)
);
}
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstBracket.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstBracket.java
index 85bd00346..c02d71524 100644
--- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstBracket.java
+++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstBracket.java
@@ -1,12 +1,14 @@
package com.hubspot.jinjava.el.ext.eager;
import com.hubspot.jinjava.el.ext.DeferredParsingException;
+import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy;
import de.odysseus.el.tree.Bindings;
import de.odysseus.el.tree.impl.ast.AstBracket;
import de.odysseus.el.tree.impl.ast.AstNode;
import javax.el.ELContext;
public class EagerAstBracket extends AstBracket implements EvalResultHolder {
+
protected Object evalResult;
protected boolean hasEvalResult;
@@ -63,7 +65,7 @@ public String getPartiallyResolved(
Bindings bindings,
ELContext context,
DeferredParsingException deferredParsingException,
- boolean preserveIdentifier
+ IdentifierPreservationStrategy identifierPreservationStrategy
) {
return String.format(
"%s[%s]",
@@ -72,14 +74,14 @@ public String getPartiallyResolved(
context,
(EvalResultHolder) prefix,
deferredParsingException,
- preserveIdentifier
+ identifierPreservationStrategy
),
EvalResultHolder.reconstructNode(
bindings,
context,
(EvalResultHolder) property,
deferredParsingException,
- false
+ IdentifierPreservationStrategy.RESOLVING
)
);
}
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstChoice.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstChoice.java
index dd5d0472f..b4bbd43dc 100644
--- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstChoice.java
+++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstChoice.java
@@ -2,6 +2,7 @@
import com.hubspot.jinjava.el.NoInvokeELContext;
import com.hubspot.jinjava.el.ext.DeferredParsingException;
+import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy;
import de.odysseus.el.tree.Bindings;
import de.odysseus.el.tree.impl.ast.AstChoice;
import de.odysseus.el.tree.impl.ast.AstNode;
@@ -9,6 +10,7 @@
import javax.el.ELException;
public class EagerAstChoice extends AstChoice implements EvalResultHolder {
+
protected Object evalResult;
protected boolean hasEvalResult;
protected final EvalResultHolder question;
@@ -46,7 +48,12 @@ public Object eval(Bindings bindings, ELContext context) throws ELException {
}
throw new DeferredParsingException(
this,
- getPartiallyResolved(bindings, context, e, false)
+ getPartiallyResolved(
+ bindings,
+ context,
+ e,
+ IdentifierPreservationStrategy.RESOLVING
+ )
);
}
}
@@ -72,7 +79,7 @@ public String getPartiallyResolved(
Bindings bindings,
ELContext context,
DeferredParsingException deferredParsingException,
- boolean preserveIdentifier
+ IdentifierPreservationStrategy identifierPreservationStrategy
) {
return (
EvalResultHolder.reconstructNode(
@@ -80,7 +87,7 @@ public String getPartiallyResolved(
context,
question,
deferredParsingException,
- false
+ IdentifierPreservationStrategy.RESOLVING
) +
" ? " +
EvalResultHolder.reconstructNode(
@@ -88,7 +95,7 @@ public String getPartiallyResolved(
new NoInvokeELContext(context),
yes,
deferredParsingException,
- preserveIdentifier
+ identifierPreservationStrategy
) +
" : " +
EvalResultHolder.reconstructNode(
@@ -96,7 +103,7 @@ public String getPartiallyResolved(
new NoInvokeELContext(context),
no,
deferredParsingException,
- preserveIdentifier
+ identifierPreservationStrategy
)
);
}
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstDict.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstDict.java
index 48302a4a1..8bdff2434 100644
--- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstDict.java
+++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstDict.java
@@ -1,8 +1,9 @@
package com.hubspot.jinjava.el.ext.eager;
+import com.hubspot.jinjava.el.HasInterpreter;
import com.hubspot.jinjava.el.ext.AstDict;
import com.hubspot.jinjava.el.ext.DeferredParsingException;
-import com.hubspot.jinjava.el.ext.ExtendedParser;
+import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy;
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
import com.hubspot.jinjava.util.EagerExpressionResolver;
import de.odysseus.el.tree.Bindings;
@@ -13,6 +14,7 @@
import javax.el.ELContext;
public class EagerAstDict extends AstDict implements EvalResultHolder {
+
protected Object evalResult;
protected boolean hasEvalResult;
@@ -34,54 +36,50 @@ public String getPartiallyResolved(
Bindings bindings,
ELContext context,
DeferredParsingException deferredParsingException,
- boolean preserveIdentifier
+ IdentifierPreservationStrategy identifierPreservationStrategy
) {
- JinjavaInterpreter interpreter = (JinjavaInterpreter) context
- .getELResolver()
- .getValue(context, null, ExtendedParser.INTERPRETER);
+ JinjavaInterpreter interpreter = ((HasInterpreter) context).interpreter();
StringJoiner joiner = new StringJoiner(", ");
- dict.forEach(
- (key, value) -> {
- StringJoiner kvJoiner = new StringJoiner(": ");
- if (key instanceof AstIdentifier) {
- kvJoiner.add(((AstIdentifier) key).getName());
- } else if (key instanceof EvalResultHolder) {
- kvJoiner.add(
- EvalResultHolder.reconstructNode(
- bindings,
- context,
- (EvalResultHolder) key,
- deferredParsingException,
+ dict.forEach((key, value) -> {
+ StringJoiner kvJoiner = new StringJoiner(": ");
+ if (key instanceof AstIdentifier) {
+ kvJoiner.add(((AstIdentifier) key).getName());
+ } else if (key instanceof EvalResultHolder) {
+ kvJoiner.add(
+ EvalResultHolder.reconstructNode(
+ bindings,
+ context,
+ (EvalResultHolder) key,
+ deferredParsingException,
+ IdentifierPreservationStrategy.preserving(
!interpreter.getConfig().getLegacyOverrides().isEvaluateMapKeys()
)
- );
- } else {
- kvJoiner.add(
- EagerExpressionResolver.getValueAsJinjavaStringSafe(
- key.eval(bindings, context)
- )
- );
- }
- if (value instanceof EvalResultHolder) {
- kvJoiner.add(
- EvalResultHolder.reconstructNode(
- bindings,
- context,
- (EvalResultHolder) value,
- deferredParsingException,
- preserveIdentifier
- )
- );
- } else {
- kvJoiner.add(
- EagerExpressionResolver.getValueAsJinjavaStringSafe(
- value.eval(bindings, context)
- )
- );
- }
- joiner.add(kvJoiner.toString());
+ )
+ );
+ } else {
+ kvJoiner.add(
+ EagerExpressionResolver.getValueAsJinjavaStringSafe(key.eval(bindings, context))
+ );
}
- );
+ if (value instanceof EvalResultHolder) {
+ kvJoiner.add(
+ EvalResultHolder.reconstructNode(
+ bindings,
+ context,
+ (EvalResultHolder) value,
+ deferredParsingException,
+ identifierPreservationStrategy
+ )
+ );
+ } else {
+ kvJoiner.add(
+ EagerExpressionResolver.getValueAsJinjavaStringSafe(
+ value.eval(bindings, context)
+ )
+ );
+ }
+ joiner.add(kvJoiner.toString());
+ });
String joined = joiner.toString();
if (joined.endsWith("}")) {
// prevent 2 closing braces from being interpreted as a closing expression token
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstDot.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstDot.java
index 862d9620d..67638b1c5 100644
--- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstDot.java
+++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstDot.java
@@ -1,6 +1,7 @@
package com.hubspot.jinjava.el.ext.eager;
import com.hubspot.jinjava.el.ext.DeferredParsingException;
+import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy;
import de.odysseus.el.tree.Bindings;
import de.odysseus.el.tree.impl.ast.AstDot;
import de.odysseus.el.tree.impl.ast.AstNode;
@@ -8,6 +9,7 @@
import javax.el.ELException;
public class EagerAstDot extends AstDot implements EvalResultHolder {
+
protected Object evalResult;
protected boolean hasEvalResult;
protected final EvalResultHolder base;
@@ -52,11 +54,17 @@ public String getPartiallyResolved(
Bindings bindings,
ELContext context,
DeferredParsingException e,
- boolean preserveIdentifier
+ IdentifierPreservationStrategy identifierPreservationStrategy
) {
return String.format(
"%s.%s",
- EvalResultHolder.reconstructNode(bindings, context, base, e, preserveIdentifier),
+ EvalResultHolder.reconstructNode(
+ bindings,
+ context,
+ base,
+ e,
+ identifierPreservationStrategy
+ ),
property
);
}
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstIdentifier.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstIdentifier.java
index a7b631e4a..86ef61009 100644
--- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstIdentifier.java
+++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstIdentifier.java
@@ -1,11 +1,13 @@
package com.hubspot.jinjava.el.ext.eager;
import com.hubspot.jinjava.el.ext.DeferredParsingException;
+import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy;
import de.odysseus.el.tree.Bindings;
import de.odysseus.el.tree.impl.ast.AstIdentifier;
import javax.el.ELContext;
public class EagerAstIdentifier extends AstIdentifier implements EvalResultHolder {
+
protected Object evalResult;
protected boolean hasEvalResult;
@@ -43,7 +45,7 @@ public String getPartiallyResolved(
Bindings bindings,
ELContext context,
DeferredParsingException deferredParsingException,
- boolean preserveIdentifier
+ IdentifierPreservationStrategy identifierPreservationStrategy
) {
return getName();
}
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstList.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstList.java
index 6ca90e4a1..fc1272366 100644
--- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstList.java
+++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstList.java
@@ -2,12 +2,14 @@
import com.hubspot.jinjava.el.ext.AstList;
import com.hubspot.jinjava.el.ext.DeferredParsingException;
+import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy;
import de.odysseus.el.tree.Bindings;
import de.odysseus.el.tree.impl.ast.AstParameters;
import java.util.StringJoiner;
import javax.el.ELContext;
public class EagerAstList extends AstList implements EvalResultHolder {
+
protected Object evalResult;
protected boolean hasEvalResult;
@@ -45,7 +47,7 @@ public String getPartiallyResolved(
Bindings bindings,
ELContext context,
DeferredParsingException deferredParsingException,
- boolean preserveIdentifier
+ IdentifierPreservationStrategy identifierPreservationStrategy
) {
StringJoiner joiner = new StringJoiner(", ");
for (int i = 0; i < elements.getCardinality(); i++) {
@@ -55,7 +57,7 @@ public String getPartiallyResolved(
context,
(EvalResultHolder) elements.getChild(i),
deferredParsingException,
- preserveIdentifier
+ identifierPreservationStrategy
)
);
}
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstMacroFunction.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstMacroFunction.java
index 5dd0cca4d..85d2bb8fd 100644
--- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstMacroFunction.java
+++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstMacroFunction.java
@@ -1,21 +1,22 @@
package com.hubspot.jinjava.el.ext.eager;
+import com.hubspot.jinjava.el.HasInterpreter;
import com.hubspot.jinjava.el.ext.AstMacroFunction;
+import com.hubspot.jinjava.el.ext.DeferredInvocationResolutionException;
import com.hubspot.jinjava.el.ext.DeferredParsingException;
-import com.hubspot.jinjava.el.ext.ExtendedParser;
+import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy;
import com.hubspot.jinjava.interpret.Context.TemporaryValueClosable;
import com.hubspot.jinjava.interpret.DeferredValueException;
-import com.hubspot.jinjava.interpret.JinjavaInterpreter;
import de.odysseus.el.tree.Bindings;
import de.odysseus.el.tree.impl.ast.AstParameters;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
-import java.util.StringJoiner;
import javax.el.ELContext;
import javax.el.ELException;
public class EagerAstMacroFunction extends AstMacroFunction implements EvalResultHolder {
+
protected Object evalResult;
protected boolean hasEvalResult;
// instanceof AstParameters
@@ -53,7 +54,13 @@ public Object eval(Bindings bindings, ELContext context) {
);
throw new DeferredParsingException(
this,
- getPartiallyResolved(bindings, context, e, true) // Need this to always be true because the macro function may modify the identifier
+ getPartiallyResolved(
+ bindings,
+ context,
+ e,
+ IdentifierPreservationStrategy.PRESERVING
+ ), // Need this to always be true because the macro function may modify the identifier
+ IdentifierPreservationStrategy.PRESERVING
);
}
}
@@ -64,19 +71,16 @@ protected Object invoke(
ELContext context,
Object base,
Method method
- )
- throws InvocationTargetException, IllegalAccessException {
+ ) throws InvocationTargetException, IllegalAccessException {
Class>[] types = method.getParameterTypes();
Object[] params = null;
if (types.length > 0) {
// This is just the AstFunction.invoke, but surrounded with this try-with-resources
try (
- TemporaryValueClosable c = (
- (JinjavaInterpreter) context
- .getELResolver()
- .getValue(context, null, ExtendedParser.INTERPRETER)
- ).getContext()
- .withPartialMacroEvaluation(false)
+ TemporaryValueClosable c =
+ ((HasInterpreter) context).interpreter()
+ .getContext()
+ .withPartialMacroEvaluation(false)
) {
params = new Object[types.length];
int varargIndex;
@@ -145,28 +149,25 @@ public String getPartiallyResolved(
Bindings bindings,
ELContext context,
DeferredParsingException deferredParsingException,
- boolean preserveIdentifier
+ IdentifierPreservationStrategy identifierPreservationStrategy
) {
- StringBuilder sb = new StringBuilder();
- sb.append(getName());
- try {
- StringJoiner paramString = new StringJoiner(", ");
- for (int i = 0; i < ((AstParameters) params).getCardinality(); i++) {
- paramString.add(
- EvalResultHolder.reconstructNode(
- bindings,
- context,
- (EvalResultHolder) ((AstParameters) params).getChild(i),
- deferredParsingException,
- preserveIdentifier
- )
+ if (deferredParsingException instanceof DeferredInvocationResolutionException) {
+ return deferredParsingException.getDeferredEvalResult();
+ }
+ String paramString;
+ if (EvalResultHolder.exceptionMatchesNode(deferredParsingException, params)) {
+ paramString = deferredParsingException.getDeferredEvalResult();
+ } else {
+ paramString =
+ params.getPartiallyResolved(
+ bindings,
+ context,
+ deferredParsingException,
+ identifierPreservationStrategy
);
- }
- sb.append(String.format("(%s)", paramString));
- } catch (DeferredParsingException dpe) {
- sb.append(String.format("(%s)", dpe.getDeferredEvalResult()));
}
- return sb.toString();
+
+ return (getName() + String.format("(%s)", paramString));
}
@Override
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstMethod.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstMethod.java
index 366195214..aec8fa234 100644
--- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstMethod.java
+++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstMethod.java
@@ -1,17 +1,18 @@
package com.hubspot.jinjava.el.ext.eager;
+import com.hubspot.jinjava.el.ext.DeferredInvocationResolutionException;
import com.hubspot.jinjava.el.ext.DeferredParsingException;
+import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy;
import com.hubspot.jinjava.interpret.DeferredValueException;
-import com.hubspot.jinjava.util.EagerExpressionResolver;
import de.odysseus.el.tree.Bindings;
import de.odysseus.el.tree.impl.ast.AstMethod;
-import de.odysseus.el.tree.impl.ast.AstNode;
import de.odysseus.el.tree.impl.ast.AstParameters;
import de.odysseus.el.tree.impl.ast.AstProperty;
import javax.el.ELContext;
import javax.el.ELException;
public class EagerAstMethod extends AstMethod implements EvalResultHolder {
+
protected Object evalResult;
protected boolean hasEvalResult;
// instanceof AstProperty
@@ -43,7 +44,13 @@ public Object eval(Bindings bindings, ELContext context) {
);
throw new DeferredParsingException(
this,
- getPartiallyResolved(bindings, context, e, true) // Need this to always be true because the method may modify the identifier
+ getPartiallyResolved(
+ bindings,
+ context,
+ e,
+ IdentifierPreservationStrategy.PRESERVING
+ ), // Need this to always be true because the method may modify the identifier
+ IdentifierPreservationStrategy.PRESERVING
);
}
}
@@ -73,33 +80,30 @@ public String getPartiallyResolved(
Bindings bindings,
ELContext context,
DeferredParsingException deferredParsingException,
- boolean preserveIdentifier
+ IdentifierPreservationStrategy identifierPreservationStrategy
) {
+ if (deferredParsingException instanceof DeferredInvocationResolutionException) {
+ return deferredParsingException.getDeferredEvalResult();
+ }
String propertyResult;
propertyResult =
(property).getPartiallyResolved(
bindings,
context,
deferredParsingException,
- preserveIdentifier
+ identifierPreservationStrategy
);
String paramString;
- if (
- deferredParsingException != null &&
- deferredParsingException.getSourceNode() == params
- ) {
+ if (EvalResultHolder.exceptionMatchesNode(deferredParsingException, params)) {
paramString = deferredParsingException.getDeferredEvalResult();
} else {
- try {
- paramString =
- EagerExpressionResolver.getValueAsJinjavaStringSafe(
- ((AstNode) params).eval(bindings, context)
- );
- // remove brackets so they can get replaced with parentheses
- paramString = paramString.substring(1, paramString.length() - 1);
- } catch (DeferredParsingException e) {
- paramString = e.getDeferredEvalResult();
- }
+ paramString =
+ params.getPartiallyResolved(
+ bindings,
+ context,
+ deferredParsingException,
+ identifierPreservationStrategy
+ );
}
return (propertyResult + String.format("(%s)", paramString));
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstNamedParameter.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstNamedParameter.java
index 8d1a23c61..0cedd9839 100644
--- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstNamedParameter.java
+++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstNamedParameter.java
@@ -2,6 +2,7 @@
import com.hubspot.jinjava.el.ext.AstNamedParameter;
import com.hubspot.jinjava.el.ext.DeferredParsingException;
+import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy;
import de.odysseus.el.tree.Bindings;
import de.odysseus.el.tree.impl.ast.AstIdentifier;
import de.odysseus.el.tree.impl.ast.AstNode;
@@ -10,6 +11,7 @@
public class EagerAstNamedParameter
extends AstNamedParameter
implements EvalResultHolder {
+
protected boolean hasEvalResult;
protected Object evalResult;
protected final AstIdentifier name;
@@ -39,7 +41,7 @@ public String getPartiallyResolved(
Bindings bindings,
ELContext context,
DeferredParsingException deferredParsingException,
- boolean preserveIdentifier
+ IdentifierPreservationStrategy identifierPreservationStrategy
) {
return String.format(
"%s=%s",
@@ -49,7 +51,7 @@ public String getPartiallyResolved(
context,
value,
deferredParsingException,
- false
+ IdentifierPreservationStrategy.RESOLVING
)
);
}
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstNested.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstNested.java
index 317167c11..03d016175 100644
--- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstNested.java
+++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstNested.java
@@ -1,6 +1,7 @@
package com.hubspot.jinjava.el.ext.eager;
import com.hubspot.jinjava.el.ext.DeferredParsingException;
+import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy;
import de.odysseus.el.tree.Bindings;
import de.odysseus.el.tree.Node;
import de.odysseus.el.tree.impl.ast.AstNode;
@@ -11,6 +12,7 @@
* AstNested is final so this decorates AstRightValue.
*/
public class EagerAstNested extends AstRightValue implements EvalResultHolder {
+
protected Object evalResult;
protected boolean hasEvalResult;
protected final AstNode child;
@@ -71,7 +73,7 @@ public String getPartiallyResolved(
Bindings bindings,
ELContext context,
DeferredParsingException deferredParsingException,
- boolean preserveIdentifier
+ IdentifierPreservationStrategy identifierPreservationStrategy
) {
return String.format(
"(%s)",
@@ -80,7 +82,7 @@ public String getPartiallyResolved(
context,
(EvalResultHolder) child,
deferredParsingException,
- preserveIdentifier
+ identifierPreservationStrategy
)
);
}
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstNodeDecorator.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstNodeDecorator.java
index 2b34b3071..8c978c506 100644
--- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstNodeDecorator.java
+++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstNodeDecorator.java
@@ -1,6 +1,7 @@
package com.hubspot.jinjava.el.ext.eager;
import com.hubspot.jinjava.el.ext.DeferredParsingException;
+import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy;
import de.odysseus.el.tree.Bindings;
import de.odysseus.el.tree.Node;
import de.odysseus.el.tree.impl.ast.AstNode;
@@ -14,6 +15,7 @@
* be an EvalResultHolder or wrapped with this decorator.
*/
public class EagerAstNodeDecorator extends AstNode implements EvalResultHolder {
+
private final AstNode astNode;
protected Object evalResult;
protected boolean hasEvalResult;
@@ -64,7 +66,7 @@ public String getPartiallyResolved(
Bindings bindings,
ELContext context,
DeferredParsingException deferredParsingException,
- boolean preserveIdentifier
+ IdentifierPreservationStrategy identifierPreservationStrategy
) {
return astNode.toString();
}
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstParameters.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstParameters.java
index 920bafff0..6cb9fc4ee 100644
--- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstParameters.java
+++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstParameters.java
@@ -1,9 +1,10 @@
package com.hubspot.jinjava.el.ext.eager;
+import com.hubspot.jinjava.el.HasInterpreter;
import com.hubspot.jinjava.el.ext.DeferredParsingException;
-import com.hubspot.jinjava.el.ext.ExtendedParser;
+import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy;
import com.hubspot.jinjava.interpret.Context.TemporaryValueClosable;
-import com.hubspot.jinjava.interpret.JinjavaInterpreter;
+import com.hubspot.jinjava.interpret.DeferredValueException;
import de.odysseus.el.tree.Bindings;
import de.odysseus.el.tree.impl.ast.AstNode;
import de.odysseus.el.tree.impl.ast.AstParameters;
@@ -11,8 +12,10 @@
import java.util.StringJoiner;
import java.util.stream.Collectors;
import javax.el.ELContext;
+import javax.el.ELException;
public class EagerAstParameters extends AstParameters implements EvalResultHolder {
+
protected Object evalResult;
protected boolean hasEvalResult;
protected final List nodes;
@@ -36,18 +39,29 @@ private EagerAstParameters(List nodes, boolean convertedToEvalResultHol
@Override
public Object[] eval(Bindings bindings, ELContext context) {
try (
- TemporaryValueClosable c = (
- (JinjavaInterpreter) context
- .getELResolver()
- .getValue(context, null, ExtendedParser.INTERPRETER)
- ).getContext()
- .withPartialMacroEvaluation(false)
+ TemporaryValueClosable c =
+ ((HasInterpreter) context).interpreter()
+ .getContext()
+ .withPartialMacroEvaluation(false)
) {
- return (Object[]) EvalResultHolder.super.eval(
- () -> super.eval(bindings, context),
- bindings,
- context
- );
+ try {
+ setEvalResult(super.eval(bindings, context));
+ return (Object[]) checkEvalResultSize(context);
+ } catch (DeferredValueException | ELException originalException) {
+ DeferredParsingException e = EvalResultHolder.convertToDeferredParsingException(
+ originalException
+ );
+ throw new DeferredParsingException(
+ this,
+ getPartiallyResolved(
+ bindings,
+ context,
+ e,
+ IdentifierPreservationStrategy.PRESERVING
+ ), // Need this to always be true because a function may modify the identifier
+ IdentifierPreservationStrategy.PRESERVING
+ );
+ }
}
}
@@ -56,23 +70,22 @@ public String getPartiallyResolved(
Bindings bindings,
ELContext context,
DeferredParsingException deferredParsingException,
- boolean preserveIdentifier
+ IdentifierPreservationStrategy identifierPreservationStrategy
) {
StringJoiner joiner = new StringJoiner(", ");
nodes
.stream()
.map(node -> (EvalResultHolder) node)
- .forEach(
- node ->
- joiner.add(
- EvalResultHolder.reconstructNode(
- bindings,
- context,
- node,
- deferredParsingException,
- false
- )
+ .forEach(node ->
+ joiner.add(
+ EvalResultHolder.reconstructNode(
+ bindings,
+ context,
+ node,
+ deferredParsingException,
+ identifierPreservationStrategy
)
+ )
);
return joiner.toString();
}
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstRangeBracket.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstRangeBracket.java
index 0f4f8d4f6..81771d3a4 100644
--- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstRangeBracket.java
+++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstRangeBracket.java
@@ -2,11 +2,13 @@
import com.hubspot.jinjava.el.ext.AstRangeBracket;
import com.hubspot.jinjava.el.ext.DeferredParsingException;
+import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy;
import de.odysseus.el.tree.Bindings;
import de.odysseus.el.tree.impl.ast.AstNode;
import javax.el.ELContext;
public class EagerAstRangeBracket extends AstRangeBracket implements EvalResultHolder {
+
protected Object evalResult;
protected boolean hasEvalResult;
@@ -42,7 +44,7 @@ public String getPartiallyResolved(
Bindings bindings,
ELContext context,
DeferredParsingException deferredParsingException,
- boolean preserveIdentifier
+ IdentifierPreservationStrategy identifierPreservationStrategy
) {
return (
EvalResultHolder.reconstructNode(
@@ -50,7 +52,7 @@ public String getPartiallyResolved(
context,
(EvalResultHolder) prefix,
deferredParsingException,
- preserveIdentifier
+ identifierPreservationStrategy
) +
"[" +
EvalResultHolder.reconstructNode(
@@ -58,7 +60,7 @@ public String getPartiallyResolved(
context,
(EvalResultHolder) property,
deferredParsingException,
- false
+ IdentifierPreservationStrategy.RESOLVING
) +
":" +
EvalResultHolder.reconstructNode(
@@ -66,7 +68,7 @@ public String getPartiallyResolved(
context,
(EvalResultHolder) rangeMax,
deferredParsingException,
- false
+ IdentifierPreservationStrategy.RESOLVING
) +
"]"
);
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstRoot.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstRoot.java
index d785cd63d..1b8f1314a 100644
--- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstRoot.java
+++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstRoot.java
@@ -8,6 +8,7 @@
import javax.el.ValueReference;
public class EagerAstRoot extends AstNode {
+
private AstNode rootNode;
public EagerAstRoot(AstNode rootNode) {
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstTuple.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstTuple.java
index e0c4b86b4..46d6aa4d2 100644
--- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstTuple.java
+++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstTuple.java
@@ -2,12 +2,14 @@
import com.hubspot.jinjava.el.ext.AstTuple;
import com.hubspot.jinjava.el.ext.DeferredParsingException;
+import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy;
import de.odysseus.el.tree.Bindings;
import de.odysseus.el.tree.impl.ast.AstParameters;
import java.util.StringJoiner;
import javax.el.ELContext;
public class EagerAstTuple extends AstTuple implements EvalResultHolder {
+
protected Object evalResult;
protected boolean hasEvalResult;
@@ -29,7 +31,7 @@ public String getPartiallyResolved(
Bindings bindings,
ELContext context,
DeferredParsingException deferredParsingException,
- boolean preserveIdentifier
+ IdentifierPreservationStrategy identifierPreservationStrategy
) {
StringJoiner joiner = new StringJoiner(", ");
for (int i = 0; i < elements.getCardinality(); i++) {
@@ -39,7 +41,7 @@ public String getPartiallyResolved(
context,
(EvalResultHolder) elements.getChild(i),
deferredParsingException,
- preserveIdentifier
+ identifierPreservationStrategy
)
);
}
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstUnary.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstUnary.java
index ffdbdfc36..da38314b9 100644
--- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstUnary.java
+++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerAstUnary.java
@@ -1,12 +1,14 @@
package com.hubspot.jinjava.el.ext.eager;
import com.hubspot.jinjava.el.ext.DeferredParsingException;
+import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy;
import de.odysseus.el.tree.Bindings;
import de.odysseus.el.tree.impl.ast.AstNode;
import de.odysseus.el.tree.impl.ast.AstUnary;
import javax.el.ELContext;
public class EagerAstUnary extends AstUnary implements EvalResultHolder {
+
protected Object evalResult;
protected boolean hasEvalResult;
protected final EvalResultHolder child;
@@ -36,7 +38,7 @@ public String getPartiallyResolved(
Bindings bindings,
ELContext context,
DeferredParsingException deferredParsingException,
- boolean preserveIdentifier
+ IdentifierPreservationStrategy identifierPreservationStrategy
) {
return (
operator.toString() +
@@ -45,7 +47,7 @@ public String getPartiallyResolved(
context,
child,
deferredParsingException,
- false
+ IdentifierPreservationStrategy.RESOLVING
)
);
}
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerExtendedParser.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerExtendedParser.java
index 5ca383afd..29f53e2b7 100644
--- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerExtendedParser.java
+++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EagerExtendedParser.java
@@ -198,4 +198,9 @@ protected AstList createAstList(AstParameters parameters)
protected AstParameters createAstParameters(List nodes) {
return new EagerAstParameters(nodes);
}
+
+ @Override
+ protected boolean shouldUseFilterChainOptimization() {
+ return false;
+ }
}
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/EvalResultHolder.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/EvalResultHolder.java
index 30de67e77..f58d05d38 100644
--- a/src/main/java/com/hubspot/jinjava/el/ext/eager/EvalResultHolder.java
+++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/EvalResultHolder.java
@@ -1,9 +1,12 @@
package com.hubspot.jinjava.el.ext.eager;
+import com.hubspot.jinjava.el.HasInterpreter;
import com.hubspot.jinjava.el.ext.DeferredParsingException;
import com.hubspot.jinjava.el.ext.ExtendedParser;
+import com.hubspot.jinjava.el.ext.IdentifierPreservationStrategy;
import com.hubspot.jinjava.interpret.DeferredValueException;
-import com.hubspot.jinjava.interpret.JinjavaInterpreter;
+import com.hubspot.jinjava.interpret.MetaContextVariables;
+import com.hubspot.jinjava.interpret.PartiallyDeferredValue;
import com.hubspot.jinjava.util.EagerExpressionResolver;
import de.odysseus.el.tree.Bindings;
import de.odysseus.el.tree.impl.ast.AstIdentifier;
@@ -34,7 +37,12 @@ default Object eval(
);
throw new DeferredParsingException(
this,
- getPartiallyResolved(bindings, context, e, false)
+ getPartiallyResolved(
+ bindings,
+ context,
+ e,
+ IdentifierPreservationStrategy.RESOLVING
+ )
);
}
}
@@ -44,12 +52,7 @@ default Object checkEvalResultSize(ELContext context) {
if (
evalResult instanceof Collection &&
((Collection>) evalResult).size() > 100 && // TODO make size configurable
- (
- (JinjavaInterpreter) context
- .getELResolver()
- .getValue(context, null, ExtendedParser.INTERPRETER)
- ).getContext()
- .isDeferLargeObjects()
+ ((HasInterpreter) context).interpreter().getContext().isDeferLargeObjects()
) {
throw new DeferredValueException("Collection too big");
}
@@ -60,7 +63,7 @@ String getPartiallyResolved(
Bindings bindings,
ELContext context,
DeferredParsingException deferredParsingException,
- boolean preserveIdentifier
+ IdentifierPreservationStrategy identifierPreservationStrategy
);
static String reconstructNode(
@@ -68,19 +71,25 @@ static String reconstructNode(
ELContext context,
EvalResultHolder astNode,
DeferredParsingException exception,
- boolean preserveIdentifier
+ IdentifierPreservationStrategy preserveIdentifier
) {
if (astNode == null) {
return "";
}
- preserveIdentifier |=
+ if (
astNode instanceof AstIdentifier &&
- ExtendedParser.INTERPRETER.equals(((AstIdentifier) astNode).getName());
+ ExtendedParser.INTERPRETER.equals(((AstIdentifier) astNode).getName())
+ ) {
+ return ExtendedParser.INTERPRETER;
+ }
+ preserveIdentifier =
+ IdentifierPreservationStrategy.preserving(preserveIdentifier.isPreserving());
if (
- preserveIdentifier &&
+ preserveIdentifier.isPreserving() &&
!astNode.hasEvalResult() &&
- !(exception != null && exception.getSourceNode() == astNode)
+ !(exceptionMatchesNode(exception, astNode))
) {
+ // Evaluate to determine if the result is primitive. If so, we don't need to preserve the identifier
try {
EagerExpressionResolver.getValueAsJinjavaStringSafe(
((AstNode) astNode).eval(bindings, context)
@@ -89,10 +98,18 @@ static String reconstructNode(
}
Object evalResult = astNode.getEvalResult();
if (
- !preserveIdentifier ||
- (astNode.hasEvalResult() && EagerExpressionResolver.isPrimitive(evalResult))
+ exceptionMatchesNode(exception, astNode) &&
+ exception.getIdentifierPreservationStrategy().isPreserving()
+ ) {
+ return exception.getDeferredEvalResult();
+ }
+ if (
+ !preserveIdentifier.isPreserving() ||
+ (astNode.hasEvalResult() &&
+ (EagerExpressionResolver.isPrimitive(evalResult) ||
+ evalResult instanceof PartiallyDeferredValue))
) {
- if (exception != null && exception.getSourceNode() == astNode) {
+ if (exceptionMatchesNode(exception, astNode)) {
return exception.getDeferredEvalResult();
}
if (!astNode.hasEvalResult()) {
@@ -104,9 +121,27 @@ static String reconstructNode(
}
try {
return EagerExpressionResolver.getValueAsJinjavaStringSafe(evalResult);
- } catch (DeferredValueException ignored) {}
+ } catch (DeferredValueException e) {
+ if (astNode instanceof AstIdentifier) {
+ String name = ((AstIdentifier) astNode).getName();
+ if (
+ MetaContextVariables.isMetaContextVariable(
+ name,
+ ((HasInterpreter) context).interpreter().getContext()
+ )
+ ) {
+ return name;
+ }
+ throw e;
+ }
+ }
}
- return astNode.getPartiallyResolved(bindings, context, exception, true);
+ return astNode.getPartiallyResolved(
+ bindings,
+ context,
+ exception,
+ IdentifierPreservationStrategy.PRESERVING
+ );
}
static DeferredParsingException convertToDeferredParsingException(
@@ -127,4 +162,14 @@ static DeferredParsingException convertToDeferredParsingException(
}
return null;
}
+
+ static boolean exceptionMatchesNode(
+ DeferredParsingException deferredParsingException,
+ Object astNode
+ ) {
+ return (
+ deferredParsingException != null &&
+ deferredParsingException.getSourceNode() == astNode
+ );
+ }
}
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/MacroFunctionTempVariable.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/MacroFunctionTempVariable.java
new file mode 100644
index 000000000..32fe8bd43
--- /dev/null
+++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/MacroFunctionTempVariable.java
@@ -0,0 +1,45 @@
+package com.hubspot.jinjava.el.ext.eager;
+
+import com.hubspot.jinjava.objects.serialization.PyishBlockSetSerializable;
+import java.util.Objects;
+
+public class MacroFunctionTempVariable implements PyishBlockSetSerializable {
+
+ private static final String CONTEXT_KEY_PREFIX = "__macro_%s_%d_temp_variable_%d__";
+ private final String deferredResult;
+
+ public MacroFunctionTempVariable(String deferredResult) {
+ this.deferredResult = deferredResult;
+ }
+
+ public static String getVarName(String macroFunctionName, int hashCode, int callCount) {
+ return String.format(
+ CONTEXT_KEY_PREFIX,
+ macroFunctionName,
+ Math.abs(hashCode),
+ callCount
+ );
+ }
+
+ @Override
+ public String getBlockSetBody() {
+ return deferredResult;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ MacroFunctionTempVariable that = (MacroFunctionTempVariable) o;
+ return deferredResult.equals(that.deferredResult);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(deferredResult);
+ }
+}
diff --git a/src/main/java/com/hubspot/jinjava/el/ext/eager/RenderFlatTempVariable.java b/src/main/java/com/hubspot/jinjava/el/ext/eager/RenderFlatTempVariable.java
new file mode 100644
index 000000000..682d3d5ca
--- /dev/null
+++ b/src/main/java/com/hubspot/jinjava/el/ext/eager/RenderFlatTempVariable.java
@@ -0,0 +1,40 @@
+package com.hubspot.jinjava.el.ext.eager;
+
+import com.hubspot.jinjava.objects.serialization.PyishBlockSetSerializable;
+import java.util.Objects;
+
+public class RenderFlatTempVariable implements PyishBlockSetSerializable {
+
+ private static final String CONTEXT_KEY_PREFIX = "__render_%d_temp_variable__";
+ private final String deferredResult;
+
+ public RenderFlatTempVariable(String deferredResult) {
+ this.deferredResult = deferredResult;
+ }
+
+ public static String getVarName(String result) {
+ return String.format(CONTEXT_KEY_PREFIX, Math.abs(result.hashCode() >> 1));
+ }
+
+ @Override
+ public String getBlockSetBody() {
+ return deferredResult;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ RenderFlatTempVariable that = (RenderFlatTempVariable) o;
+ return deferredResult.equals(that.deferredResult);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(deferredResult);
+ }
+}
diff --git a/src/main/java/com/hubspot/jinjava/features/BuiltInFeatures.java b/src/main/java/com/hubspot/jinjava/features/BuiltInFeatures.java
new file mode 100644
index 000000000..87b34a6b8
--- /dev/null
+++ b/src/main/java/com/hubspot/jinjava/features/BuiltInFeatures.java
@@ -0,0 +1,12 @@
+package com.hubspot.jinjava.features;
+
+public interface BuiltInFeatures {
+ String WHITESPACE_REQUIRED_WITHIN_TOKENS = "whitespace_required_within_tokens";
+ String FIXED_DATE_TIME_FILTER_NULL_ARG = "FIXED_DATE_TIME_FILTER_NULL_ARG";
+ String ECHO_UNDEFINED = "echoUndefined";
+ String PREVENT_ACCIDENTAL_EXPRESSIONS = "PREVENT_ACCIDENTAL_EXPRESSIONS";
+ String IGNORE_NESTED_INTERPRETATION_PARSE_ERRORS =
+ "IGNORE_NESTED_INTERPRETATION_PARSE_ERRORS";
+ String OUTPUT_UNDEFINED_VARIABLES_ERROR = "OUTPUT_UNDEFINED_VARIABLES_ERROR";
+ String INTEGER_SET_TO_LONG_CONVERSION = "INTEGER_SET_TO_LONG_CONVERSION";
+}
diff --git a/src/main/java/com/hubspot/jinjava/features/DateTimeFeatureActivationStrategy.java b/src/main/java/com/hubspot/jinjava/features/DateTimeFeatureActivationStrategy.java
new file mode 100644
index 000000000..c50e4f679
--- /dev/null
+++ b/src/main/java/com/hubspot/jinjava/features/DateTimeFeatureActivationStrategy.java
@@ -0,0 +1,31 @@
+package com.hubspot.jinjava.features;
+
+import com.hubspot.jinjava.interpret.Context;
+import java.time.ZonedDateTime;
+
+public class DateTimeFeatureActivationStrategy implements FeatureActivationStrategy {
+
+ private final ZonedDateTime activateAt;
+
+ public static DateTimeFeatureActivationStrategy of(ZonedDateTime activateAt) {
+ return new DateTimeFeatureActivationStrategy(activateAt);
+ }
+
+ private DateTimeFeatureActivationStrategy(ZonedDateTime activateAt) {
+ this.activateAt = activateAt;
+ }
+
+ @Override
+ public boolean isActive(Context context) {
+ return ZonedDateTime.now().isAfter(activateAt);
+ }
+
+ @Override
+ public boolean isActive() {
+ return false; // Not usable without context
+ }
+
+ public ZonedDateTime getActivateAt() {
+ return activateAt;
+ }
+}
diff --git a/src/main/java/com/hubspot/jinjava/features/FeatureActivationStrategy.java b/src/main/java/com/hubspot/jinjava/features/FeatureActivationStrategy.java
new file mode 100644
index 000000000..6ba27b206
--- /dev/null
+++ b/src/main/java/com/hubspot/jinjava/features/FeatureActivationStrategy.java
@@ -0,0 +1,11 @@
+package com.hubspot.jinjava.features;
+
+import com.hubspot.jinjava.interpret.Context;
+
+public interface FeatureActivationStrategy {
+ default boolean isActive(Context context) {
+ return isActive();
+ }
+
+ boolean isActive();
+}
diff --git a/src/main/java/com/hubspot/jinjava/features/FeatureConfig.java b/src/main/java/com/hubspot/jinjava/features/FeatureConfig.java
new file mode 100644
index 000000000..7038592ce
--- /dev/null
+++ b/src/main/java/com/hubspot/jinjava/features/FeatureConfig.java
@@ -0,0 +1,57 @@
+package com.hubspot.jinjava.features;
+
+import com.google.common.collect.ImmutableMap;
+import com.hubspot.jinjava.interpret.Context;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+public class FeatureConfig {
+
+ Map features;
+
+ private FeatureConfig(Map features) {
+ this.features = ImmutableMap.copyOf(features);
+ }
+
+ public FeatureActivationStrategy getFeature(String name) {
+ return features.getOrDefault(name, FeatureStrategies.INACTIVE);
+ }
+
+ public static FeatureConfig.Builder newBuilder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+
+ private final Map features = new HashMap<>();
+
+ @Deprecated
+ public Builder add(String name, Function strategyFunction) {
+ features.put(
+ name,
+ new FeatureActivationStrategy() {
+ @Override
+ public boolean isActive(Context context) {
+ return strategyFunction.apply(context);
+ }
+
+ @Override
+ public boolean isActive() {
+ return false;
+ }
+ }
+ );
+ return this;
+ }
+
+ public Builder add(String name, FeatureActivationStrategy strategy) {
+ features.put(name, strategy);
+ return this;
+ }
+
+ public FeatureConfig build() {
+ return new FeatureConfig(features);
+ }
+ }
+}
diff --git a/src/main/java/com/hubspot/jinjava/features/FeatureStrategies.java b/src/main/java/com/hubspot/jinjava/features/FeatureStrategies.java
new file mode 100644
index 000000000..8ae4025f4
--- /dev/null
+++ b/src/main/java/com/hubspot/jinjava/features/FeatureStrategies.java
@@ -0,0 +1,7 @@
+package com.hubspot.jinjava.features;
+
+public class FeatureStrategies {
+
+ public static final FeatureActivationStrategy INACTIVE = () -> false;
+ public static final FeatureActivationStrategy ACTIVE = () -> true;
+}
diff --git a/src/main/java/com/hubspot/jinjava/features/Features.java b/src/main/java/com/hubspot/jinjava/features/Features.java
new file mode 100644
index 000000000..5e311f322
--- /dev/null
+++ b/src/main/java/com/hubspot/jinjava/features/Features.java
@@ -0,0 +1,24 @@
+package com.hubspot.jinjava.features;
+
+import com.hubspot.jinjava.interpret.Context;
+
+public class Features {
+
+ private final FeatureConfig featureConfig;
+
+ public Features(FeatureConfig featureConfig) {
+ this.featureConfig = featureConfig;
+ }
+
+ public boolean isActive(String featureName, Context context) {
+ return getActivationStrategy(featureName).isActive(context);
+ }
+
+ public boolean isActive(String featureName) {
+ return getActivationStrategy(featureName).isActive();
+ }
+
+ public FeatureActivationStrategy getActivationStrategy(String featureName) {
+ return featureConfig.getFeature(featureName);
+ }
+}
diff --git a/src/main/java/com/hubspot/jinjava/interpret/AutoCloseableSupplier.java b/src/main/java/com/hubspot/jinjava/interpret/AutoCloseableSupplier.java
new file mode 100644
index 000000000..12a7b8dfe
--- /dev/null
+++ b/src/main/java/com/hubspot/jinjava/interpret/AutoCloseableSupplier.java
@@ -0,0 +1,68 @@
+package com.hubspot.jinjava.interpret;
+
+import com.google.common.base.Suppliers;
+import com.hubspot.jinjava.interpret.AutoCloseableSupplier.AutoCloseableImpl;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+public class AutoCloseableSupplier implements Supplier> {
+
+ public static AutoCloseableSupplier of(T tSupplier) {
+ return of(() -> tSupplier, ignored -> {});
+ }
+
+ public static AutoCloseableSupplier of(
+ Supplier tSupplier,
+ Consumer closeConsumer
+ ) {
+ return new AutoCloseableSupplier<>(
+ Suppliers.memoize(() -> new AutoCloseableImpl<>(tSupplier.get(), closeConsumer))
+ );
+ }
+
+ private final Supplier> autoCloseableImplWrapper;
+
+ private AutoCloseableSupplier(Supplier> autoCloseableImplWrapper) {
+ this.autoCloseableImplWrapper = autoCloseableImplWrapper;
+ }
+
+ @Override
+ public AutoCloseableImpl get() {
+ return autoCloseableImplWrapper.get();
+ }
+
+ public T dangerouslyGetWithoutClosing() {
+ return autoCloseableImplWrapper.get().value();
+ }
+
+ public AutoCloseableSupplier map(Function mapper) {
+ return new AutoCloseableSupplier<>(() -> {
+ T t = autoCloseableImplWrapper.get().value();
+ return new AutoCloseableImpl<>(
+ mapper.apply(t),
+ r -> autoCloseableImplWrapper.get().closeConsumer.accept(t)
+ );
+ });
+ }
+
+ public static class AutoCloseableImpl implements java.lang.AutoCloseable {
+
+ private final T t;
+ private final Consumer closeConsumer;
+
+ protected AutoCloseableImpl(T t, Consumer closeConsumer) {
+ this.t = t;
+ this.closeConsumer = closeConsumer;
+ }
+
+ public T value() {
+ return t;
+ }
+
+ @Override
+ public void close() {
+ closeConsumer.accept(t);
+ }
+ }
+}
diff --git a/src/main/java/com/hubspot/jinjava/interpret/CallStack.java b/src/main/java/com/hubspot/jinjava/interpret/CallStack.java
index 1b876a4f9..f67d96d6b 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/CallStack.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/CallStack.java
@@ -1,9 +1,11 @@
package com.hubspot.jinjava.interpret;
+import com.hubspot.algebra.Result;
import java.util.Optional;
import java.util.Stack;
public class CallStack {
+
private final CallStack parent;
private final Class extends TagCycleException> exceptionClass;
private final Stack stack = new Stack<>();
@@ -110,6 +112,57 @@ public boolean isEmpty() {
return stack.empty() && (parent == null || parent.isEmpty());
}
+ public AutoCloseableSupplier> closeablePush(
+ String path,
+ int lineNumber,
+ int startPosition
+ ) {
+ return AutoCloseableSupplier.of(
+ () -> {
+ try {
+ push(path, lineNumber, startPosition);
+ return Result.ok(path);
+ } catch (TagCycleException e) {
+ return Result.err(e);
+ }
+ },
+ result -> result.ifOk(ok -> pop())
+ );
+ }
+
+ public AutoCloseableSupplier closeablePushWithoutCycleCheck(
+ String path,
+ int lineNumber,
+ int startPosition
+ ) {
+ return AutoCloseableSupplier.of(
+ () -> {
+ pushWithoutCycleCheck(path, lineNumber, startPosition);
+ return path;
+ },
+ ignored -> pop()
+ );
+ }
+
+ public AutoCloseableSupplier> closeablePushWithMaxDepth(
+ String path,
+ int maxDepth,
+ int lineNumber,
+ int startPosition
+ ) {
+ return AutoCloseableSupplier.of(
+ () -> {
+ try {
+ pushWithMaxDepth(path, maxDepth, lineNumber, startPosition);
+ return Result.ok(path);
+ } catch (TagCycleException e) {
+ return Result.err(e);
+ }
+ },
+ result -> result.ifOk(ok -> pop())
+ );
+ }
+
private void pushToStack(String path, int lineNumber, int startPosition) {
if (isEmpty()) {
topLineNumber = lineNumber;
diff --git a/src/main/java/com/hubspot/jinjava/interpret/CannotReconstructValueException.java b/src/main/java/com/hubspot/jinjava/interpret/CannotReconstructValueException.java
new file mode 100644
index 000000000..64a7e2d83
--- /dev/null
+++ b/src/main/java/com/hubspot/jinjava/interpret/CannotReconstructValueException.java
@@ -0,0 +1,13 @@
+package com.hubspot.jinjava.interpret;
+
+import com.google.common.annotations.Beta;
+
+@Beta
+public class CannotReconstructValueException extends DeferredValueException {
+
+ public static final String CANNOT_RECONSTRUCT_MESSAGE = "Cannot reconstruct value";
+
+ public CannotReconstructValueException(String key) {
+ super(String.format("%s: %s", CANNOT_RECONSTRUCT_MESSAGE, key));
+ }
+}
diff --git a/src/main/java/com/hubspot/jinjava/interpret/CollectionTooBigException.java b/src/main/java/com/hubspot/jinjava/interpret/CollectionTooBigException.java
index 465be10aa..bae6f52a8 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/CollectionTooBigException.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/CollectionTooBigException.java
@@ -1,6 +1,7 @@
package com.hubspot.jinjava.interpret;
public class CollectionTooBigException extends RuntimeException {
+
private final int maxSize;
private final int size;
diff --git a/src/main/java/com/hubspot/jinjava/interpret/Context.java b/src/main/java/com/hubspot/jinjava/interpret/Context.java
index 8e4d2d30d..a5fbf8544 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/Context.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/Context.java
@@ -16,11 +16,14 @@
package com.hubspot.jinjava.interpret;
+import com.google.common.annotations.Beta;
import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.SetMultimap;
+import com.google.common.collect.Sets;
+import com.hubspot.jinjava.interpret.AutoCloseableSupplier.AutoCloseableImpl;
import com.hubspot.jinjava.lib.Importable;
-import com.hubspot.jinjava.lib.expression.DefaultExpressionStrategy;
import com.hubspot.jinjava.lib.expression.ExpressionStrategy;
import com.hubspot.jinjava.lib.exptest.ExpTest;
import com.hubspot.jinjava.lib.exptest.ExpTestLibrary;
@@ -29,15 +32,16 @@
import com.hubspot.jinjava.lib.fn.ELFunctionDefinition;
import com.hubspot.jinjava.lib.fn.FunctionLibrary;
import com.hubspot.jinjava.lib.fn.MacroFunction;
+import com.hubspot.jinjava.lib.tag.ForTag;
import com.hubspot.jinjava.lib.tag.Tag;
import com.hubspot.jinjava.lib.tag.TagLibrary;
import com.hubspot.jinjava.lib.tag.eager.DeferredToken;
+import com.hubspot.jinjava.mode.EagerExecutionMode;
import com.hubspot.jinjava.tree.Node;
import com.hubspot.jinjava.util.DeferredValueUtils;
import com.hubspot.jinjava.util.ScopeMap;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -49,6 +53,7 @@
import java.util.stream.Collectors;
public class Context extends ScopeMap {
+
public static final String GLOBAL_MACROS_SCOPE_KEY = "__macros__";
public static final String IMPORT_RESOURCE_PATH_KEY = "import_resource_path";
public static final String DEFERRED_IMPORT_RESOURCE_PATH_KEY =
@@ -60,11 +65,11 @@ public class Context extends ScopeMap {
private Map> disabled;
public boolean isValidationMode() {
- return validationMode;
+ return contextConfiguration.isValidationMode();
}
public Context setValidationMode(boolean validationMode) {
- this.validationMode = validationMode;
+ contextConfiguration = contextConfiguration.withValidationMode(validationMode);
return this;
}
@@ -72,7 +77,7 @@ public enum Library {
EXP_TEST,
FILTER,
FUNCTION,
- TAG
+ TAG,
}
private final CallStack extendPathStack;
@@ -87,6 +92,8 @@ public enum Library {
private final Set resolvedFunctions = new HashSet<>();
private Set deferredNodes = new HashSet<>();
+
+ @Beta
private Set deferredTokens = new HashSet<>();
private final ExpTestLibrary expTestLibrary;
@@ -94,8 +101,6 @@ public enum Library {
private final FunctionLibrary functionLibrary;
private final TagLibrary tagLibrary;
- private ExpressionStrategy expressionStrategy = new DefaultExpressionStrategy();
-
private final Context parent;
private int renderDepth = -1;
@@ -103,15 +108,9 @@ public enum Library {
private List extends Node> superBlock;
private final Stack renderStack = new Stack<>();
-
- private boolean validationMode = false;
- private boolean deferredExecutionMode = false;
- private boolean deferLargeObjects = false;
- private boolean throwInterpreterErrors = false;
- private boolean partialMacroEvaluation = false;
- private boolean unwrapRawOverride = false;
- private DynamicVariableResolver dynamicVariableResolver = null;
+ private ContextConfiguration contextConfiguration = ContextConfiguration.of();
private final Set metaContextVariables; // These variable names aren't tracked in eager execution
+ private final Set overriddenNonMetaContextVariables;
private Node currentNode;
public Context() {
@@ -193,7 +192,7 @@ public Context(
: parent == null ? null : parent.getCurrentPathStack();
if (disabled == null) {
- disabled = new HashMap<>();
+ disabled = ImmutableMap.of();
}
this.expTestLibrary =
@@ -204,14 +203,10 @@ public Context(
new FunctionLibrary(parent == null, disabled.get(Library.FUNCTION));
this.metaContextVariables =
parent == null ? new HashSet<>() : parent.metaContextVariables;
+ this.overriddenNonMetaContextVariables =
+ parent == null ? new HashSet<>() : parent.overriddenNonMetaContextVariables;
if (parent != null) {
- this.expressionStrategy = parent.expressionStrategy;
- this.partialMacroEvaluation = parent.partialMacroEvaluation;
- this.unwrapRawOverride = parent.unwrapRawOverride;
- this.dynamicVariableResolver = parent.dynamicVariableResolver;
- this.deferredExecutionMode = parent.deferredExecutionMode;
- this.deferLargeObjects = parent.deferLargeObjects;
- this.throwInterpreterErrors = parent.throwInterpreterErrors;
+ this.contextConfiguration = parent.contextConfiguration;
}
}
@@ -343,18 +338,79 @@ public void addResolvedFunction(String function) {
}
}
+ /**
+ * @deprecated Use {@link MetaContextVariables#isMetaContextVariable(String, Context)}
+ */
+ @Deprecated
+ @Beta
public Set getMetaContextVariables() {
return metaContextVariables;
}
+ @Beta
+ Set getComputedMetaContextVariables() {
+ return Sets.difference(metaContextVariables, overriddenNonMetaContextVariables);
+ }
+
+ @Beta
+ public void addMetaContextVariables(Collection variables) {
+ metaContextVariables.addAll(variables);
+ }
+
+ Set getNonMetaContextVariables() {
+ return overriddenNonMetaContextVariables;
+ }
+
+ @Beta
+ public void addNonMetaContextVariables(Collection variables) {
+ overriddenNonMetaContextVariables.addAll(
+ variables
+ .stream()
+ .filter(var -> !EagerExecutionMode.STATIC_META_CONTEXT_VARIABLES.contains(var))
+ .collect(Collectors.toList())
+ );
+ }
+
+ @Beta
+ public void removeNonMetaContextVariables(Collection variables) {
+ overriddenNonMetaContextVariables.removeAll(variables);
+ }
+
public void handleDeferredNode(Node node) {
+ if (
+ JinjavaInterpreter
+ .getCurrentMaybe()
+ .map(interpreter -> interpreter.getConfig().getExecutionMode().useEagerParser())
+ .orElse(false)
+ ) {
+ addDeferredNodeRecursively(node);
+ } else {
+ handleDeferredNodeAndDeferVariables(node);
+ }
+ }
+
+ private void addDeferredNodeRecursively(Node node) {
deferredNodes.add(node);
- Set deferredProps = DeferredValueUtils.findAndMarkDeferredProperties(this);
if (getParent() != null) {
Context parent = getParent();
- //Ignore global context
+ // Ignore global context
+ if (parent.getParent() != null) {
+ getParent().handleDeferredNode(node);
+ }
+ }
+ }
+
+ private void handleDeferredNodeAndDeferVariables(Node node) {
+ deferredNodes.add(node);
+ Set deferredProps = DeferredValueUtils.findAndMarkDeferredProperties(
+ this,
+ node
+ );
+ if (getParent() != null) {
+ Context parent = getParent();
+ // Ignore global context
if (parent.getParent() != null) {
- //Place deferred values on the parent context
+ // Place deferred values on the parent context
deferredProps
.stream()
.filter(key -> !parent.containsKey(key))
@@ -368,6 +424,7 @@ public Set getDeferredNodes() {
return ImmutableSet.copyOf(deferredNodes);
}
+ @Beta
public void checkNumberOfDeferredTokens() {
Context secondToLastContext = this;
if (parent != null) {
@@ -375,39 +432,25 @@ public void checkNumberOfDeferredTokens() {
secondToLastContext = secondToLastContext.parent;
}
}
- int maxNumDeferredTokens = JinjavaInterpreter
+ int currentNumDeferredTokens = secondToLastContext.deferredTokens.size();
+ JinjavaInterpreter
.getCurrentMaybe()
.map(i -> i.getConfig().getMaxNumDeferredTokens())
- .orElse(1000);
- if (secondToLastContext.deferredTokens.size() >= maxNumDeferredTokens) {
- throw new DeferredValueException(
- "Too many Deferred Tokens, max is " + maxNumDeferredTokens
- );
- }
+ .filter(maxNumDeferredTokens -> currentNumDeferredTokens >= maxNumDeferredTokens)
+ .ifPresent(maxNumDeferredTokens -> {
+ throw new DeferredValueException(
+ "Too many Deferred Tokens, max is " + maxNumDeferredTokens
+ );
+ });
}
+ @Beta
public void handleDeferredToken(DeferredToken deferredToken) {
- deferredTokens.add(deferredToken);
-
- if (
- deferredToken.getImportResourcePath() == null ||
- deferredToken.getImportResourcePath().equals(get(Context.IMPORT_RESOURCE_PATH_KEY))
- ) {
- DeferredValueUtils.findAndMarkDeferredProperties(this, deferredToken);
- }
- if (getParent() != null) {
- Context parent = getParent();
- //Ignore global context
- if (parent.getParent() != null) {
- parent.handleDeferredToken(deferredToken);
- } else {
- checkNumberOfDeferredTokens();
- }
- }
+ deferredToken.addTo(this);
}
+ @Beta
public void removeDeferredTokens(Collection toRemove) {
- deferredTokens.removeAll(toRemove);
if (getParent() != null) {
Context parent = getParent();
//Ignore global context
@@ -415,8 +458,10 @@ public void removeDeferredTokens(Collection toRemove) {
parent.removeDeferredTokens(toRemove);
}
}
+ deferredTokens.removeAll(toRemove);
}
+ @Beta
public Set getDeferredTokens() {
return deferredTokens;
}
@@ -538,10 +583,11 @@ public void registerFilter(Filter f) {
}
public boolean isFunctionDisabled(String name) {
- return (
- disabled != null &&
- disabled.getOrDefault(Library.FUNCTION, Collections.emptySet()).contains(name)
- );
+ if (disabled == null) {
+ return false;
+ }
+ Set disabledFunctions = disabled.get(Library.FUNCTION);
+ return disabledFunctions != null && disabledFunctions.contains(name);
}
public ELFunctionDefinition getFunction(String name) {
@@ -561,10 +607,13 @@ public Collection getAllFunctions() {
if (parent != null) {
fns.addAll(parent.getAllFunctions());
}
-
- final Set disabledFunctions = disabled == null
- ? new HashSet<>()
- : disabled.getOrDefault(Library.FUNCTION, new HashSet<>());
+ if (disabled == null) {
+ return fns;
+ }
+ Set disabledFunctions = disabled.get(Library.FUNCTION);
+ if (disabledFunctions == null) {
+ return fns;
+ }
return fns
.stream()
.filter(f -> !disabledFunctions.contains(f.getName()))
@@ -601,21 +650,23 @@ public void registerTag(Tag t) {
}
public DynamicVariableResolver getDynamicVariableResolver() {
- return dynamicVariableResolver;
+ return contextConfiguration.getDynamicVariableResolver();
}
public void setDynamicVariableResolver(
final DynamicVariableResolver dynamicVariableResolver
) {
- this.dynamicVariableResolver = dynamicVariableResolver;
+ contextConfiguration =
+ contextConfiguration.withDynamicVariableResolver(dynamicVariableResolver);
}
public ExpressionStrategy getExpressionStrategy() {
- return expressionStrategy;
+ return contextConfiguration.getExpressionStrategy();
}
public void setExpressionStrategy(ExpressionStrategy expressionStrategy) {
- this.expressionStrategy = expressionStrategy;
+ contextConfiguration =
+ contextConfiguration.withExpressionStrategy(expressionStrategy);
}
public Optional getImportResourceAlias() {
@@ -630,6 +681,10 @@ public CallStack getImportPathStack() {
return importPathStack;
}
+ public CallStack getFromPathStack() {
+ return fromStack;
+ }
+
public CallStack getIncludePathStack() {
return includePathStack;
}
@@ -646,10 +701,12 @@ public CallStack getCurrentPathStack() {
return currentPathStack;
}
+ @Deprecated
public void pushFromStack(String path, int lineNumber, int startPosition) {
fromStack.push(path, lineNumber, startPosition);
}
+ @Deprecated
public void popFromStack() {
fromStack.pop();
}
@@ -670,10 +727,17 @@ public void setRenderDepth(int renderDepth) {
this.renderDepth = renderDepth;
}
+ public AutoCloseableSupplier closeablePushRenderStack(String template) {
+ renderStack.push(template);
+ return AutoCloseableSupplier.of(() -> template, t -> renderStack.pop());
+ }
+
+ @Deprecated
public void pushRenderStack(String template) {
renderStack.push(template);
}
+ @Deprecated
public String popRenderStack() {
return renderStack.pop();
}
@@ -701,20 +765,21 @@ public SetMultimap getDependencies() {
}
public boolean isDeferredExecutionMode() {
- return deferredExecutionMode;
+ return contextConfiguration.isDeferredExecutionMode();
}
public Context setDeferredExecutionMode(boolean deferredExecutionMode) {
- this.deferredExecutionMode = deferredExecutionMode;
+ contextConfiguration =
+ contextConfiguration.withDeferredExecutionMode(deferredExecutionMode);
return this;
}
public boolean isDeferLargeObjects() {
- return deferLargeObjects;
+ return contextConfiguration.isDeferLargeObjects();
}
public Context setDeferLargeObjects(boolean deferLargeObjects) {
- this.deferLargeObjects = deferLargeObjects;
+ contextConfiguration = contextConfiguration.withDeferLargeObjects(deferLargeObjects);
return this;
}
@@ -722,27 +787,80 @@ public TemporaryValueClosable withDeferLargeObjects(
boolean deferLargeObjects
) {
TemporaryValueClosable temporaryValueClosable = new TemporaryValueClosable<>(
- this.deferLargeObjects,
+ isDeferLargeObjects(),
this::setDeferLargeObjects
);
- this.deferLargeObjects = deferLargeObjects;
+ setDeferLargeObjects(deferLargeObjects);
return temporaryValueClosable;
}
+ @Deprecated
public boolean getThrowInterpreterErrors() {
- return throwInterpreterErrors;
+ ErrorHandlingStrategy errorHandlingStrategy = getErrorHandlingStrategy();
+ return (
+ errorHandlingStrategy.getFatalErrorStrategy() ==
+ ErrorHandlingStrategy.TemplateErrorTypeHandlingStrategy.THROW_EXCEPTION
+ );
}
+ @Deprecated
public void setThrowInterpreterErrors(boolean throwInterpreterErrors) {
- this.throwInterpreterErrors = throwInterpreterErrors;
+ contextConfiguration =
+ contextConfiguration.withErrorHandlingStrategy(
+ ErrorHandlingStrategy
+ .builder()
+ .setFatalErrorStrategy(
+ throwInterpreterErrors
+ ? ErrorHandlingStrategy.TemplateErrorTypeHandlingStrategy.THROW_EXCEPTION
+ : ErrorHandlingStrategy.TemplateErrorTypeHandlingStrategy.ADD_ERROR
+ )
+ .setNonFatalErrorStrategy(
+ throwInterpreterErrors
+ ? ErrorHandlingStrategy.TemplateErrorTypeHandlingStrategy.IGNORE // Deprecated, warnings are ignored when doing eager expression resolving
+ : ErrorHandlingStrategy.TemplateErrorTypeHandlingStrategy.ADD_ERROR
+ )
+ .build()
+ );
+ }
+
+ @Deprecated
+ public TemporaryValueClosable withThrowInterpreterErrors() {
+ TemporaryValueClosable temporaryValueClosable = new TemporaryValueClosable<>(
+ getThrowInterpreterErrors(),
+ this::setThrowInterpreterErrors
+ );
+ setThrowInterpreterErrors(true);
+ return temporaryValueClosable;
+ }
+
+ public ErrorHandlingStrategy getErrorHandlingStrategy() {
+ return contextConfiguration.getErrorHandlingStrategy();
+ }
+
+ public void setErrorHandlingStrategy(ErrorHandlingStrategy errorHandlingStrategy) {
+ contextConfiguration =
+ contextConfiguration.withErrorHandlingStrategy(errorHandlingStrategy);
+ }
+
+ public TemporaryValueClosable withErrorHandlingStrategy(
+ ErrorHandlingStrategy errorHandlingStrategy
+ ) {
+ TemporaryValueClosable temporaryValueClosable =
+ new TemporaryValueClosable<>(
+ getErrorHandlingStrategy(),
+ this::setErrorHandlingStrategy
+ );
+ setErrorHandlingStrategy(errorHandlingStrategy);
+ return temporaryValueClosable;
}
public boolean isPartialMacroEvaluation() {
- return partialMacroEvaluation;
+ return contextConfiguration.isPartialMacroEvaluation();
}
public void setPartialMacroEvaluation(boolean partialMacroEvaluation) {
- this.partialMacroEvaluation = partialMacroEvaluation;
+ contextConfiguration =
+ contextConfiguration.withPartialMacroEvaluation(partialMacroEvaluation);
}
public TemporaryValueClosable withPartialMacroEvaluation() {
@@ -753,42 +871,54 @@ public TemporaryValueClosable withPartialMacroEvaluation(
boolean partialMacroEvaluation
) {
TemporaryValueClosable temporaryValueClosable = new TemporaryValueClosable<>(
- this.partialMacroEvaluation,
+ isPartialMacroEvaluation(),
this::setPartialMacroEvaluation
);
- this.partialMacroEvaluation = partialMacroEvaluation;
+ setPartialMacroEvaluation(partialMacroEvaluation);
return temporaryValueClosable;
}
public boolean isUnwrapRawOverride() {
- return unwrapRawOverride;
+ return contextConfiguration.isUnwrapRawOverride();
}
public void setUnwrapRawOverride(boolean unwrapRawOverride) {
- this.unwrapRawOverride = unwrapRawOverride;
+ contextConfiguration = contextConfiguration.withUnwrapRawOverride(unwrapRawOverride);
}
public TemporaryValueClosable withUnwrapRawOverride() {
+ return withUnwrapRawOverride(true);
+ }
+
+ public TemporaryValueClosable withUnwrapRawOverride(boolean value) {
TemporaryValueClosable temporaryValueClosable = new TemporaryValueClosable<>(
- this.unwrapRawOverride,
+ isUnwrapRawOverride(),
this::setUnwrapRawOverride
);
- this.unwrapRawOverride = true;
+ setUnwrapRawOverride(value);
return temporaryValueClosable;
}
- public static class TemporaryValueClosable implements AutoCloseable {
- private final T previousValue;
- private final Consumer resetValueConsumer;
+ public static class TemporaryValueClosable extends AutoCloseableImpl {
private TemporaryValueClosable(T previousValue, Consumer resetValueConsumer) {
- this.previousValue = previousValue;
- this.resetValueConsumer = resetValueConsumer;
+ super(previousValue, resetValueConsumer);
}
- @Override
- public void close() {
- resetValueConsumer.accept(previousValue);
+ public static TemporaryValueClosable noOp() {
+ return new NoOpTemporaryValueClosable<>();
+ }
+
+ private static class NoOpTemporaryValueClosable extends TemporaryValueClosable {
+
+ private NoOpTemporaryValueClosable() {
+ super(null, null);
+ }
+
+ @Override
+ public void close() {
+ // No-op
+ }
}
}
@@ -799,4 +929,8 @@ public Node getCurrentNode() {
public void setCurrentNode(final Node currentNode) {
this.currentNode = currentNode;
}
+
+ public boolean isInForLoop() {
+ return get(ForTag.LOOP) != null;
+ }
}
diff --git a/src/main/java/com/hubspot/jinjava/interpret/ContextConfiguration.java b/src/main/java/com/hubspot/jinjava/interpret/ContextConfiguration.java
new file mode 100644
index 000000000..b902efdef
--- /dev/null
+++ b/src/main/java/com/hubspot/jinjava/interpret/ContextConfiguration.java
@@ -0,0 +1,54 @@
+package com.hubspot.jinjava.interpret;
+
+import com.hubspot.jinjava.JinjavaImmutableStyle;
+import com.hubspot.jinjava.lib.expression.DefaultExpressionStrategy;
+import com.hubspot.jinjava.lib.expression.ExpressionStrategy;
+import javax.annotation.Nullable;
+import org.immutables.value.Value.Default;
+import org.immutables.value.Value.Immutable;
+
+@Immutable(singleton = true)
+@JinjavaImmutableStyle
+public interface ContextConfiguration extends WithContextConfiguration {
+ @Default
+ default ExpressionStrategy getExpressionStrategy() {
+ return new DefaultExpressionStrategy();
+ }
+
+ @Nullable
+ DynamicVariableResolver getDynamicVariableResolver();
+
+ @Default
+ default boolean isValidationMode() {
+ return false;
+ }
+
+ @Default
+ default boolean isDeferredExecutionMode() {
+ return false;
+ }
+
+ @Default
+ default boolean isDeferLargeObjects() {
+ return false;
+ }
+
+ @Default
+ default boolean isPartialMacroEvaluation() {
+ return false;
+ }
+
+ @Default
+ default boolean isUnwrapRawOverride() {
+ return false;
+ }
+
+ @Default
+ default ErrorHandlingStrategy getErrorHandlingStrategy() {
+ return ImmutableErrorHandlingStrategy.of();
+ }
+
+ static ContextConfiguration of() {
+ return ImmutableContextConfiguration.of();
+ }
+}
diff --git a/src/main/java/com/hubspot/jinjava/interpret/DeferredLazyReference.java b/src/main/java/com/hubspot/jinjava/interpret/DeferredLazyReference.java
index 5a1ce338c..43d0561bd 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/DeferredLazyReference.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/DeferredLazyReference.java
@@ -1,7 +1,17 @@
package com.hubspot.jinjava.interpret;
-public class DeferredLazyReference implements DeferredValue {
+import com.google.common.annotations.Beta;
+
+@Beta
+public class DeferredLazyReference
+ implements DeferredValue, Cloneable, OneTimeReconstructible {
+
private final LazyReference lazyReference;
+ private boolean reconstructed;
+
+ private DeferredLazyReference(LazyReference lazyReference) {
+ this.lazyReference = lazyReference;
+ }
private DeferredLazyReference(Context referenceContext, String referenceKey) {
lazyReference = LazyReference.of(referenceContext, referenceKey);
@@ -15,7 +25,24 @@ public static DeferredLazyReference instance(
}
@Override
- public Object getOriginalValue() {
+ public LazyReference getOriginalValue() {
return lazyReference;
}
+
+ public boolean isReconstructed() {
+ return reconstructed;
+ }
+
+ public void setReconstructed(boolean reconstructed) {
+ this.reconstructed = reconstructed;
+ }
+
+ @Override
+ public DeferredLazyReference clone() {
+ try {
+ return (DeferredLazyReference) super.clone();
+ } catch (CloneNotSupportedException e) {
+ return new DeferredLazyReference(lazyReference);
+ }
+ }
}
diff --git a/src/main/java/com/hubspot/jinjava/interpret/DeferredLazyReferenceSource.java b/src/main/java/com/hubspot/jinjava/interpret/DeferredLazyReferenceSource.java
new file mode 100644
index 000000000..f73c1f8ee
--- /dev/null
+++ b/src/main/java/com/hubspot/jinjava/interpret/DeferredLazyReferenceSource.java
@@ -0,0 +1,36 @@
+package com.hubspot.jinjava.interpret;
+
+import com.google.common.annotations.Beta;
+
+@Beta
+public class DeferredLazyReferenceSource
+ extends DeferredValueImpl
+ implements OneTimeReconstructible {
+
+ private static final DeferredLazyReferenceSource INSTANCE =
+ new DeferredLazyReferenceSource();
+
+ private boolean reconstructed;
+
+ private DeferredLazyReferenceSource() {}
+
+ private DeferredLazyReferenceSource(Object originalValue) {
+ super(originalValue);
+ }
+
+ public static DeferredLazyReferenceSource instance() {
+ return INSTANCE;
+ }
+
+ public static DeferredLazyReferenceSource instance(Object originalValue) {
+ return new DeferredLazyReferenceSource(originalValue);
+ }
+
+ public boolean isReconstructed() {
+ return reconstructed;
+ }
+
+ public void setReconstructed(boolean reconstructed) {
+ this.reconstructed = reconstructed;
+ }
+}
diff --git a/src/main/java/com/hubspot/jinjava/interpret/DeferredMacroValueImpl.java b/src/main/java/com/hubspot/jinjava/interpret/DeferredMacroValueImpl.java
index 226757cc6..3aaaf20ec 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/DeferredMacroValueImpl.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/DeferredMacroValueImpl.java
@@ -1,6 +1,10 @@
package com.hubspot.jinjava.interpret;
+import com.google.common.annotations.Beta;
+
+@Beta
public class DeferredMacroValueImpl implements DeferredValue {
+
private static final DeferredValue INSTANCE = new DeferredMacroValueImpl();
private DeferredMacroValueImpl() {}
diff --git a/src/main/java/com/hubspot/jinjava/interpret/DeferredValue.java b/src/main/java/com/hubspot/jinjava/interpret/DeferredValue.java
index 19bc8995e..8cd6bfc58 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/DeferredValue.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/DeferredValue.java
@@ -15,4 +15,8 @@ static DeferredValue instance() {
static DeferredValue instance(Object originalValue) {
return DeferredValueImpl.instance(originalValue);
}
+
+ static DeferredValueShadow shadowInstance(Object originalValue) {
+ return DeferredValueShadow.instance(originalValue);
+ }
}
diff --git a/src/main/java/com/hubspot/jinjava/interpret/DeferredValueException.java b/src/main/java/com/hubspot/jinjava/interpret/DeferredValueException.java
index a5a37da9f..884f59e7c 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/DeferredValueException.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/DeferredValueException.java
@@ -6,6 +6,7 @@
* and instead echo its contents to the output.
*/
public class DeferredValueException extends InterpretException {
+
public static final String MESSAGE_PREFIX = "Encountered a deferred value: ";
public DeferredValueException(String message) {
diff --git a/src/main/java/com/hubspot/jinjava/interpret/DeferredValueImpl.java b/src/main/java/com/hubspot/jinjava/interpret/DeferredValueImpl.java
index a2b6e04b9..4b001ca94 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/DeferredValueImpl.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/DeferredValueImpl.java
@@ -3,25 +3,27 @@
import java.util.Objects;
public class DeferredValueImpl implements DeferredValue {
+
private static final DeferredValue INSTANCE = new DeferredValueImpl();
private Object originalValue;
- private DeferredValueImpl() {}
+ protected DeferredValueImpl() {}
- private DeferredValueImpl(Object originalValue) {
+ protected DeferredValueImpl(Object originalValue) {
this.originalValue = originalValue;
}
+ @Override
public Object getOriginalValue() {
return originalValue;
}
- public static DeferredValue instance() {
+ protected static DeferredValue instance() {
return INSTANCE;
}
- public static DeferredValue instance(Object originalValue) {
+ protected static DeferredValue instance(Object originalValue) {
return new DeferredValueImpl(originalValue);
}
diff --git a/src/main/java/com/hubspot/jinjava/interpret/DeferredValueShadow.java b/src/main/java/com/hubspot/jinjava/interpret/DeferredValueShadow.java
new file mode 100644
index 000000000..c1dddf72a
--- /dev/null
+++ b/src/main/java/com/hubspot/jinjava/interpret/DeferredValueShadow.java
@@ -0,0 +1,25 @@
+package com.hubspot.jinjava.interpret;
+
+import com.google.common.annotations.Beta;
+
+/**
+ * A deferred value which represents that a value was deferred within this context,
+ * but it is does not overwrite the actual key in which the original value resides on the context.
+ */
+@Beta
+public class DeferredValueShadow extends DeferredValueImpl {
+
+ protected DeferredValueShadow() {}
+
+ protected DeferredValueShadow(Object originalValue) {
+ super(originalValue);
+ }
+
+ protected static DeferredValueShadow instance() {
+ return new DeferredValueShadow();
+ }
+
+ protected static DeferredValueShadow instance(Object originalValue) {
+ return new DeferredValueShadow(originalValue);
+ }
+}
diff --git a/src/main/java/com/hubspot/jinjava/interpret/DisabledException.java b/src/main/java/com/hubspot/jinjava/interpret/DisabledException.java
index 072110550..d1521a2b2 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/DisabledException.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/DisabledException.java
@@ -1,6 +1,7 @@
package com.hubspot.jinjava.interpret;
public class DisabledException extends InterpretException {
+
private final String token;
public DisabledException(String token) {
diff --git a/src/main/java/com/hubspot/jinjava/interpret/DynamicVariableResolver.java b/src/main/java/com/hubspot/jinjava/interpret/DynamicVariableResolver.java
index fe3b239ef..aaee1c324 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/DynamicVariableResolver.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/DynamicVariableResolver.java
@@ -1,18 +1,18 @@
-/**
- * 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 java.util.function.Function;
-
-public interface DynamicVariableResolver extends Function {}
+/**
+ * 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 java.util.function.Function;
+
+public interface DynamicVariableResolver extends Function {}
diff --git a/src/main/java/com/hubspot/jinjava/interpret/ErrorHandlingStrategy.java b/src/main/java/com/hubspot/jinjava/interpret/ErrorHandlingStrategy.java
new file mode 100644
index 000000000..23df9c977
--- /dev/null
+++ b/src/main/java/com/hubspot/jinjava/interpret/ErrorHandlingStrategy.java
@@ -0,0 +1,46 @@
+package com.hubspot.jinjava.interpret;
+
+import com.hubspot.jinjava.JinjavaImmutableStyle;
+import org.immutables.value.Value;
+
+@Value.Immutable(singleton = true)
+@JinjavaImmutableStyle
+public interface ErrorHandlingStrategy {
+ @Value.Default
+ default TemplateErrorTypeHandlingStrategy getFatalErrorStrategy() {
+ return TemplateErrorTypeHandlingStrategy.ADD_ERROR;
+ }
+
+ @Value.Default
+ default TemplateErrorTypeHandlingStrategy getNonFatalErrorStrategy() {
+ return TemplateErrorTypeHandlingStrategy.ADD_ERROR;
+ }
+
+ enum TemplateErrorTypeHandlingStrategy {
+ IGNORE,
+ ADD_ERROR,
+ THROW_EXCEPTION,
+ }
+
+ class Builder extends ImmutableErrorHandlingStrategy.Builder {}
+
+ static Builder builder() {
+ return new Builder();
+ }
+
+ static ErrorHandlingStrategy throwAll() {
+ return ErrorHandlingStrategy
+ .builder()
+ .setFatalErrorStrategy(TemplateErrorTypeHandlingStrategy.THROW_EXCEPTION)
+ .setNonFatalErrorStrategy(TemplateErrorTypeHandlingStrategy.THROW_EXCEPTION)
+ .build();
+ }
+
+ static ErrorHandlingStrategy ignoreAll() {
+ return ErrorHandlingStrategy
+ .builder()
+ .setFatalErrorStrategy(TemplateErrorTypeHandlingStrategy.IGNORE)
+ .setNonFatalErrorStrategy(TemplateErrorTypeHandlingStrategy.IGNORE)
+ .build();
+ }
+}
diff --git a/src/main/java/com/hubspot/jinjava/interpret/ExtendsTagCycleException.java b/src/main/java/com/hubspot/jinjava/interpret/ExtendsTagCycleException.java
index cc27f4fca..3c3a63431 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/ExtendsTagCycleException.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/ExtendsTagCycleException.java
@@ -1,6 +1,7 @@
package com.hubspot.jinjava.interpret;
public class ExtendsTagCycleException extends TagCycleException {
+
private static final long serialVersionUID = 3183769038400532542L;
public ExtendsTagCycleException(String path, int lineNumber, int startPosition) {
diff --git a/src/main/java/com/hubspot/jinjava/interpret/FatalTemplateErrorsException.java b/src/main/java/com/hubspot/jinjava/interpret/FatalTemplateErrorsException.java
index c3c7a7386..41c1f69e6 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/FatalTemplateErrorsException.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/FatalTemplateErrorsException.java
@@ -8,6 +8,7 @@
* @author jstehler
*/
public class FatalTemplateErrorsException extends InterpretException {
+
private static final long serialVersionUID = 1L;
private final String template;
diff --git a/src/main/java/com/hubspot/jinjava/interpret/FromTagCycleException.java b/src/main/java/com/hubspot/jinjava/interpret/FromTagCycleException.java
index 20f395260..35715af8e 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/FromTagCycleException.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/FromTagCycleException.java
@@ -1,6 +1,7 @@
package com.hubspot.jinjava.interpret;
public class FromTagCycleException extends TagCycleException {
+
private static final long serialVersionUID = -5487642459443650227L;
public FromTagCycleException(String path, int lineNumber, int startPosition) {
diff --git a/src/main/java/com/hubspot/jinjava/interpret/ImportTagCycleException.java b/src/main/java/com/hubspot/jinjava/interpret/ImportTagCycleException.java
index 5dff0b6ca..4b37b9dc3 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/ImportTagCycleException.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/ImportTagCycleException.java
@@ -1,6 +1,7 @@
package com.hubspot.jinjava.interpret;
public class ImportTagCycleException extends TagCycleException {
+
private static final long serialVersionUID = 1092085697026161185L;
public ImportTagCycleException(String path, int lineNumber, int startPosition) {
diff --git a/src/main/java/com/hubspot/jinjava/interpret/IncludeTagCycleException.java b/src/main/java/com/hubspot/jinjava/interpret/IncludeTagCycleException.java
index 8ddbc6ae5..5060b3079 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/IncludeTagCycleException.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/IncludeTagCycleException.java
@@ -1,6 +1,7 @@
package com.hubspot.jinjava.interpret;
public class IncludeTagCycleException extends TagCycleException {
+
private static final long serialVersionUID = -5487642459443650227L;
public IncludeTagCycleException(String path, int lineNumber, int startPosition) {
diff --git a/src/main/java/com/hubspot/jinjava/interpret/InterpretException.java b/src/main/java/com/hubspot/jinjava/interpret/InterpretException.java
index 301a3a13c..476d0dd99 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/InterpretException.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/InterpretException.java
@@ -16,6 +16,7 @@
package com.hubspot.jinjava.interpret;
public class InterpretException extends RuntimeException {
+
private static final long serialVersionUID = -3471306977643126138L;
private int lineNumber = -1;
diff --git a/src/main/java/com/hubspot/jinjava/interpret/InvalidArgumentException.java b/src/main/java/com/hubspot/jinjava/interpret/InvalidArgumentException.java
index 72883c807..2c4272d85 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/InvalidArgumentException.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/InvalidArgumentException.java
@@ -3,6 +3,7 @@
import com.hubspot.jinjava.lib.Importable;
public class InvalidArgumentException extends RuntimeException {
+
private final int lineNumber;
private final int startPosition;
private final String message;
diff --git a/src/main/java/com/hubspot/jinjava/interpret/InvalidInputException.java b/src/main/java/com/hubspot/jinjava/interpret/InvalidInputException.java
index dabbcd5a3..96d39f35a 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/InvalidInputException.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/InvalidInputException.java
@@ -3,6 +3,7 @@
import com.hubspot.jinjava.lib.Importable;
public class InvalidInputException extends RuntimeException {
+
private final int lineNumber;
private final int startPosition;
private final String message;
diff --git a/src/main/java/com/hubspot/jinjava/interpret/InvalidReason.java b/src/main/java/com/hubspot/jinjava/interpret/InvalidReason.java
index 2ff1cb991..a662e493b 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/InvalidReason.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/InvalidReason.java
@@ -17,7 +17,8 @@ public enum InvalidReason {
"with value '%s' must be a valid attribute of every item in the list"
),
ENUM("with value '%s' must be one of: %s"),
- CIDR("with value '%s' must be a valid CIDR address");
+ CIDR("with value '%s' must be a valid CIDR address"),
+ LENGTH("with length '%s' exceeds maximum allowed length of '%s'");
private final String errorMessage;
diff --git a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java
index 564f9bdb8..4034ee43c 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java
@@ -24,18 +24,23 @@
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
+import com.hubspot.algebra.Result;
import com.hubspot.jinjava.Jinjava;
import com.hubspot.jinjava.JinjavaConfig;
import com.hubspot.jinjava.el.ExpressionResolver;
import com.hubspot.jinjava.el.ext.DeferredParsingException;
import com.hubspot.jinjava.el.ext.ExtendedParser;
+import com.hubspot.jinjava.features.BuiltInFeatures;
+import com.hubspot.jinjava.interpret.AutoCloseableSupplier.AutoCloseableImpl;
+import com.hubspot.jinjava.interpret.Context.TemporaryValueClosable;
import com.hubspot.jinjava.interpret.TemplateError.ErrorItem;
import com.hubspot.jinjava.interpret.TemplateError.ErrorReason;
import com.hubspot.jinjava.interpret.TemplateError.ErrorType;
import com.hubspot.jinjava.interpret.errorcategory.BasicTemplateErrorCategory;
+import com.hubspot.jinjava.lib.tag.DoTag;
import com.hubspot.jinjava.lib.tag.ExtendsTag;
-import com.hubspot.jinjava.lib.tag.SetTag;
import com.hubspot.jinjava.lib.tag.eager.EagerGenericTag;
+import com.hubspot.jinjava.loader.RelativePathResolver;
import com.hubspot.jinjava.objects.serialization.PyishObjectMapper;
import com.hubspot.jinjava.objects.serialization.PyishSerializable;
import com.hubspot.jinjava.random.ConstantZeroRandomNumberGenerator;
@@ -46,14 +51,17 @@
import com.hubspot.jinjava.tree.TreeParser;
import com.hubspot.jinjava.tree.output.BlockInfo;
import com.hubspot.jinjava.tree.output.BlockPlaceholderOutputNode;
+import com.hubspot.jinjava.tree.output.DynamicRenderedOutputNode;
import com.hubspot.jinjava.tree.output.OutputList;
import com.hubspot.jinjava.tree.output.OutputNode;
import com.hubspot.jinjava.tree.output.RenderedOutputNode;
+import com.hubspot.jinjava.util.DeferredValueUtils;
import com.hubspot.jinjava.util.EagerReconstructionUtils;
+import com.hubspot.jinjava.util.RenderLimitUtils;
import com.hubspot.jinjava.util.Variable;
import com.hubspot.jinjava.util.WhitespaceUtils;
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
@@ -67,11 +75,20 @@
import java.util.Set;
import java.util.Stack;
import java.util.concurrent.ThreadLocalRandom;
+import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
public class JinjavaInterpreter implements PyishSerializable {
+
+ public static final String IGNORED_OUTPUT_FROM_EXTENDS_NOTE =
+ "ignored_output_from_extends";
+
+ public static final String OUTPUT_UNDEFINED_VARIABLES_ERROR =
+ BuiltInFeatures.OUTPUT_UNDEFINED_VARIABLES_ERROR;
+ public static final String IGNORE_NESTED_INTERPRETATION_PARSE_ERRORS =
+ BuiltInFeatures.IGNORE_NESTED_INTERPRETATION_PARSE_ERRORS;
private final Multimap blocks = ArrayListMultimap.create();
private final LinkedList extendParentRoots = new LinkedList<>();
private final Map revertibleObjects = new HashMap<>();
@@ -87,7 +104,9 @@ public class JinjavaInterpreter implements PyishSerializable {
private int position = 0;
private int scopeDepth = 1;
private BlockInfo currentBlock;
- private final List errors = new LinkedList<>();
+ private final List errors = new ArrayList<>();
+ private final Set errorSet = new HashSet<>();
+
private static final int MAX_ERROR_SIZE = 100;
public JinjavaInterpreter(
@@ -98,7 +117,6 @@ public JinjavaInterpreter(
this.context = context;
this.config = renderConfig;
this.application = application;
-
this.config.getExecutionMode().prepareContext(this.context);
switch (config.getRandomNumberGeneratorStrategy()) {
@@ -126,6 +144,24 @@ public JinjavaInterpreter(JinjavaInterpreter orig) {
scopeDepth = orig.getScopeDepth() + 1;
}
+ public static void checkOutputSize(String string) {
+ if (isOutputTooLarge(string)) {
+ throw new OutputTooBigException(
+ getCurrent().getConfig().getMaxOutputSize(),
+ string.length()
+ );
+ }
+ }
+
+ public static boolean isOutputTooLarge(String string) {
+ Optional maxStringLength = getCurrentMaybe()
+ .map(interpreter -> interpreter.getConfig().getMaxOutputSize())
+ .filter(max -> max > 0);
+ return (
+ maxStringLength.map(max -> string != null && string.length() > max).orElse(false)
+ );
+ }
+
/**
* @deprecated use {{@link #getConfig()}}
*/
@@ -145,9 +181,9 @@ public void addBlock(String name, BlockInfo blockInfo) {
/**
* 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:
- *
+ *
*
* interpreter.enterScope();
* try (interpreter.enterScope()) {
@@ -213,6 +249,20 @@ public Node parse(String template) {
* @return rendered result
*/
public String renderFlat(String template) {
+ return renderFlat(template, config.getMaxOutputSize());
+ }
+
+ /**
+ * 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
+ * @param renderLimit
+ * stop rendering once this output length is reached
+ * @return rendered result
+ */
+ public String renderFlat(String template, long renderLimit) {
int depth = context.getRenderDepth();
try {
@@ -221,13 +271,28 @@ public String renderFlat(String template) {
return template;
} else {
context.setRenderDepth(depth + 1);
- return render(parse(template), false);
+ Node parsedNode;
+ try (
+ TemporaryValueClosable c = ignoreParseErrorsIfActivated()
+ ) {
+ parsedNode = parse(template);
+ }
+ return render(parsedNode, false, renderLimit);
}
} finally {
context.setRenderDepth(depth);
}
}
+ private TemporaryValueClosable ignoreParseErrorsIfActivated() {
+ return config
+ .getFeatures()
+ .getActivationStrategy(BuiltInFeatures.IGNORE_NESTED_INTERPRETATION_PARSE_ERRORS)
+ .isActive(context)
+ ? context.withErrorHandlingStrategy(ErrorHandlingStrategy.ignoreAll())
+ : TemporaryValueClosable.noOp();
+ }
+
/**
* Parse the given string into a root Node, and then renders it processing extend parents.
*
@@ -236,7 +301,20 @@ public String renderFlat(String template) {
* @return rendered result
*/
public String render(String template) {
- return render(parse(template), true);
+ return render(template, config.getMaxOutputSize());
+ }
+
+ /**
+ * Parse the given string into a root Node, and then renders it processing extend parents.
+ *
+ * @param template
+ * string to parse
+ * @param renderLimit
+ * stop rendering once this output length is reached
+ * @return rendered result
+ */
+ public String render(String template, long renderLimit) {
+ return render(parse(template), true, renderLimit);
}
/**
@@ -247,7 +325,19 @@ public String render(String template) {
* @return rendered result
*/
public String render(Node root) {
- return render(root, true);
+ return render(root, true, config.getMaxOutputSize());
+ }
+
+ /**
+ * Render the given root node with an option to process extend parents.
+ * Equivalent to render(root, processExtendRoots).
+ * @param root
+ * node to render
+ * @param processExtendRoots
+ * @return
+ */
+ public String render(Node root, boolean processExtendRoots) {
+ return render(root, processExtendRoots, config.getMaxOutputSize());
}
/**
@@ -257,145 +347,203 @@ public String render(Node root) {
* node to render
* @param processExtendRoots
* if true, also render all extend parents
+ * @param renderLimit
+ * stop rendering once this output length is reached
* @return rendered result
*/
- public String render(Node root, boolean processExtendRoots) {
- OutputList output = new OutputList(config.getMaxOutputSize());
-
- for (Node node : root.getChildren()) {
- lineNumber = node.getLineNumber();
- position = node.getStartPosition();
- String renderStr = node.getMaster().getImage();
- try {
- if (node instanceof ExpressionNode && context.doesRenderStackContain(renderStr)) {
- // This is a circular rendering. Stop rendering it here.
+ private String render(Node root, boolean processExtendRoots, long renderLimit) {
+ boolean pushed = false;
+ //noinspection ErrorProne
+ if (JinjavaInterpreter.getCurrent() != this) {
+ JinjavaInterpreter.pushCurrent(this);
+ pushed = true;
+ }
+ try {
+ OutputList output = new OutputList(
+ RenderLimitUtils.clampProvidedRenderLimitToConfig(renderLimit, config)
+ );
+ for (Node node : root.getChildren()) {
+ lineNumber = node.getLineNumber();
+ position = node.getStartPosition();
+ String renderStr = node.getMaster().getImage();
+ try {
+ if (
+ node instanceof ExpressionNode && context.doesRenderStackContain(renderStr)
+ ) {
+ // This is a circular rendering. Stop rendering it here.
+ addError(
+ new TemplateError(
+ ErrorType.WARNING,
+ ErrorReason.EXCEPTION,
+ ErrorItem.TAG,
+ "Rendering cycle detected: '" + renderStr + "'",
+ null,
+ getLineNumber(),
+ node.getStartPosition(),
+ null,
+ BasicTemplateErrorCategory.IMPORT_CYCLE_DETECTED,
+ ImmutableMap.of("string", renderStr)
+ )
+ );
+ output.addNode(new RenderedOutputNode(renderStr));
+ } else {
+ OutputNode out;
+ try (
+ AutoCloseableImpl closeable = context
+ .closeablePushRenderStack(renderStr)
+ .get()
+ ) {
+ try {
+ out = node.render(this);
+ } catch (DeferredValueException e) {
+ context.handleDeferredNode(node);
+ out = new RenderedOutputNode(node.getMaster().getImage());
+ }
+ }
+ output.addNode(out);
+ }
+ } catch (OutputTooBigException e) {
+ addError(TemplateError.fromOutputTooBigException(e));
+ return output.getValue();
+ } catch (CollectionTooBigException e) {
addError(
new TemplateError(
- ErrorType.WARNING,
- ErrorReason.EXCEPTION,
- ErrorItem.TAG,
- "Rendering cycle detected: '" + renderStr + "'",
+ ErrorType.FATAL,
+ ErrorReason.COLLECTION_TOO_BIG,
+ ErrorItem.OTHER,
+ ExceptionUtils.getMessage(e),
null,
- getLineNumber(),
- node.getStartPosition(),
- null,
- BasicTemplateErrorCategory.IMPORT_CYCLE_DETECTED,
- ImmutableMap.of("string", renderStr)
+ -1,
+ -1,
+ e,
+ BasicTemplateErrorCategory.UNKNOWN,
+ ImmutableMap.of()
)
);
- output.addNode(new RenderedOutputNode(renderStr));
- } else {
- OutputNode out;
- context.pushRenderStack(renderStr);
- try {
- out = node.render(this);
- } catch (DeferredValueException e) {
- context.handleDeferredNode(node);
- out = new RenderedOutputNode(node.getMaster().getImage());
- }
- context.popRenderStack();
- output.addNode(out);
+ return output.getValue();
}
- } catch (OutputTooBigException e) {
- addError(TemplateError.fromOutputTooBigException(e));
- return output.getValue();
- } catch (CollectionTooBigException e) {
- addError(
- new TemplateError(
- ErrorType.FATAL,
- ErrorReason.COLLECTION_TOO_BIG,
- ErrorItem.OTHER,
- ExceptionUtils.getMessage(e),
- null,
- -1,
- -1,
- e,
- BasicTemplateErrorCategory.UNKNOWN,
- ImmutableMap.of()
- )
- );
- return output.getValue();
}
- }
- StringBuilder ignoredOutput = new StringBuilder();
-
- // render all extend parents, keeping the last as the root output
- if (processExtendRoots) {
- Set extendPaths = new HashSet<>();
- Optional extendPath = context.getExtendPathStack().peek();
- int numDeferredTokensBefore = 0;
- while (!extendParentRoots.isEmpty()) {
- if (extendPaths.contains(extendPath.orElse(""))) {
- addError(
- TemplateError.fromException(
- new ExtendsTagCycleException(
- extendPath.orElse(""),
- context.getExtendPathStack().getTopLineNumber(),
- context.getExtendPathStack().getTopStartPosition()
+ DynamicRenderedOutputNode pathSetter = new DynamicRenderedOutputNode();
+ output.addNode(pathSetter);
+ Optional basePath = context.getCurrentPathStack().peek();
+ StringBuilder ignoredOutput = new StringBuilder();
+ boolean preserveBlocks = false;
+ // render all extend parents, keeping the last as the root output
+ if (processExtendRoots) {
+ Set extendPaths = new HashSet<>();
+ Optional extendPath = context.getExtendPathStack().peek();
+ int numDeferredTokensBefore = 0;
+ while (!extendParentRoots.isEmpty()) {
+ if (extendPaths.contains(extendPath.orElse(""))) {
+ addError(
+ TemplateError.fromException(
+ new ExtendsTagCycleException(
+ extendPath.orElse(""),
+ context.getExtendPathStack().getTopLineNumber(),
+ context.getExtendPathStack().getTopStartPosition()
+ )
)
- )
- );
- break;
- }
- extendPaths.add(extendPath.orElse(""));
- context
- .getCurrentPathStack()
- .push(
- extendPath.orElse(""),
- context.getExtendPathStack().getTopLineNumber(),
- context.getExtendPathStack().getTopStartPosition()
- );
- Node parentRoot = extendParentRoots.removeFirst();
- if (context.getDeferredTokens().size() > numDeferredTokensBefore) {
- ignoredOutput.append(
- output
- .getNodes()
- .stream()
- .filter(node -> node instanceof RenderedOutputNode)
- .map(OutputNode::getValue)
- .collect(Collectors.joining())
- );
- }
- numDeferredTokensBefore = context.getDeferredTokens().size();
- output = new OutputList(config.getMaxOutputSize());
-
- boolean hasNestedExtends = false;
- for (Node node : parentRoot.getChildren()) {
- lineNumber = node.getLineNumber() - 1; // The line number is off by one when rendering the extend parent
- position = node.getStartPosition();
- try {
- OutputNode out = node.render(this);
- output.addNode(out);
- if (isExtendsTag(node)) {
- hasNestedExtends = true;
+ );
+ break;
+ }
+ extendPaths.add(extendPath.orElse(""));
+ try (
+ AutoCloseableImpl> closeableCurrentPath =
+ context
+ .getCurrentPathStack()
+ .closeablePush(
+ extendPath.orElse(""),
+ context.getExtendPathStack().getTopLineNumber(),
+ context.getExtendPathStack().getTopStartPosition()
+ )
+ .get()
+ ) {
+ String currentPath = closeableCurrentPath
+ .value()
+ .unwrapOrElseThrow(Function.identity());
+ Node parentRoot = extendParentRoots.removeFirst();
+ if (context.getDeferredTokens().size() > numDeferredTokensBefore) {
+ ignoredOutput.append(
+ output
+ .getNodes()
+ .stream()
+ .filter(node -> node instanceof RenderedOutputNode)
+ .map(OutputNode::getValue)
+ .collect(Collectors.joining())
+ );
}
- } catch (OutputTooBigException e) {
- addError(TemplateError.fromOutputTooBigException(e));
- return output.getValue();
+ numDeferredTokensBefore = context.getDeferredTokens().size();
+ output = new OutputList(config.getMaxOutputSize());
+ output.addNode(pathSetter);
+ boolean hasNestedExtends = false;
+ for (Node node : parentRoot.getChildren()) {
+ lineNumber = node.getLineNumber() - 1; // The line number is off by one when rendering the extend parent
+ position = node.getStartPosition();
+ try {
+ OutputNode out = node.render(this);
+ output.addNode(out);
+ if (isExtendsTag(node)) {
+ hasNestedExtends = true;
+ }
+ } catch (OutputTooBigException e) {
+ addError(TemplateError.fromOutputTooBigException(e));
+ return output.getValue();
+ }
+ }
+ Optional currentExtendPath = context.getExtendPathStack().pop();
+ extendPath =
+ hasNestedExtends ? currentExtendPath : context.getExtendPathStack().peek();
+ basePath = Optional.of(currentPath);
}
}
+ preserveBlocks = (context.getDeferredTokens().size() > numDeferredTokensBefore);
+ }
- Optional currentExtendPath = context.getExtendPathStack().pop();
- extendPath =
- hasNestedExtends ? currentExtendPath : context.getExtendPathStack().peek();
- context.getCurrentPathStack().pop();
+ int numDeferredTokensBefore = context.getDeferredTokens().size();
+ resolveBlockStubs(output);
+ if (preserveBlocks) {
+ for (BlockPlaceholderOutputNode blockPlaceholder : output.getBlocks()) {
+ blockPlaceholder.resolve(
+ EagerReconstructionUtils.wrapInTag(
+ blockPlaceholder.getValue(),
+ "block %s".formatted(blockPlaceholder.getBlockName()),
+ this,
+ false
+ )
+ );
+ }
+ }
+ if (context.getDeferredTokens().size() > numDeferredTokensBefore) {
+ pathSetter.setValue(
+ EagerReconstructionUtils.buildBlockOrInlineSetTag(
+ RelativePathResolver.CURRENT_PATH_CONTEXT_KEY,
+ basePath,
+ this
+ )
+ );
}
- }
- resolveBlockStubs(output);
- if (ignoredOutput.length() > 0) {
- return (
- EagerReconstructionUtils.buildBlockSetTag(
- SetTag.IGNORED_VARIABLE_NAME,
- ignoredOutput.toString(),
- this,
- false
- ) +
- output.getValue()
- );
+ if (ignoredOutput.length() > 0) {
+ return (
+ EagerReconstructionUtils.labelWithNotes(
+ EagerReconstructionUtils.wrapInTag(
+ ignoredOutput.toString(),
+ DoTag.TAG_NAME,
+ this,
+ false
+ ),
+ IGNORED_OUTPUT_FROM_EXTENDS_NOTE,
+ this
+ ) +
+ output.getValue()
+ );
+ }
+ return output.getValue();
+ } finally {
+ if (pushed) {
+ JinjavaInterpreter.popCurrent();
+ }
}
-
- return output.getValue();
}
private void resolveBlockStubs(OutputList output) {
@@ -405,10 +553,8 @@ private void resolveBlockStubs(OutputList output) {
private boolean isExtendsTag(Node node) {
return (
node instanceof TagNode &&
- (
- ((TagNode) node).getTag() instanceof ExtendsTag ||
- isEagerExtendsTag((TagNode) node)
- )
+ (((TagNode) node).getTag() instanceof ExtendsTag ||
+ isEagerExtendsTag((TagNode) node))
);
}
@@ -419,10 +565,6 @@ private boolean isEagerExtendsTag(TagNode node) {
);
}
- @SuppressFBWarnings(
- justification = "Iterables#getFirst DOES allow null for default value",
- value = "NP_NONNULL_PARAM_VIOLATION"
- )
private void resolveBlockStubs(OutputList output, Stack blockNames) {
for (BlockPlaceholderOutputNode blockPlaceholder : output.getBlocks()) {
if (!blockNames.contains(blockPlaceholder.getBlockName())) {
@@ -438,34 +580,32 @@ private void resolveBlockStubs(OutputList output, Stack blockNames) {
currentBlock = block;
OutputList blockValueBuilder = new OutputList(config.getMaxOutputSize());
-
- for (Node child : block.getNodes()) {
- lineNumber = child.getLineNumber();
- position = child.getStartPosition();
-
- boolean pushedParentPathOntoStack = false;
- if (
- block.getParentPath().isPresent() &&
- !getContext().getCurrentPathStack().contains(block.getParentPath().get())
- ) {
- getContext()
- .getCurrentPathStack()
- .push(
- block.getParentPath().get(),
- block.getParentLineNo(),
- block.getParentPosition()
- );
- pushedParentPathOntoStack = true;
+ DynamicRenderedOutputNode prefix = new DynamicRenderedOutputNode();
+ blockValueBuilder.addNode(prefix);
+ int numDeferredTokensBefore = context.getDeferredTokens().size();
+
+ try (
+ AutoCloseableImpl parentPathPush = conditionallyPushParentPath(block)
+ .get()
+ ) {
+ if (parentPathPush.value()) {
lineNumber--; // The line number is off by one when rendering the block from the parent template
}
- blockValueBuilder.addNode(child.render(this));
+ for (Node child : block.getNodes()) {
+ lineNumber = child.getLineNumber();
+ position = child.getStartPosition();
- if (pushedParentPathOntoStack) {
- getContext().getCurrentPathStack().pop();
+ blockValueBuilder.addNode(child.render(this));
+ }
+ if (context.getDeferredTokens().size() > numDeferredTokensBefore) {
+ EagerReconstructionUtils.reconstructPathAroundBlock(
+ prefix,
+ blockValueBuilder,
+ this
+ );
}
}
-
blockNames.push(blockPlaceholder.getBlockName());
resolveBlockStubs(blockValueBuilder, blockNames);
blockNames.pop();
@@ -483,6 +623,24 @@ private void resolveBlockStubs(OutputList output, Stack blockNames) {
}
}
+ private AutoCloseableSupplier conditionallyPushParentPath(BlockInfo block) {
+ if (
+ block.getParentPath().isPresent() &&
+ !getContext().getCurrentPathStack().contains(block.getParentPath().get())
+ ) {
+ return getContext()
+ .getCurrentPathStack()
+ .closeablePush(
+ block.getParentPath().get(),
+ block.getParentLineNo(),
+ block.getParentPosition()
+ )
+ .map(path -> true);
+ } else {
+ return AutoCloseableSupplier.of(false);
+ }
+ }
+
/**
* 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.
*
@@ -505,7 +663,7 @@ public Object retraceVariable(String variable, int lineNumber, int startPosition
obj = context.getDynamicVariableResolver().apply(varName);
}
if (obj != null) {
- if (obj instanceof DeferredValue && !(obj instanceof PartiallyDeferredValue)) {
+ if (DeferredValueUtils.isFullyDeferred(obj)) {
if (config.getExecutionMode().useEagerParser()) {
throw new DeferredParsingException(this, variable);
} else {
@@ -513,6 +671,28 @@ public Object retraceVariable(String variable, int lineNumber, int startPosition
}
}
obj = var.resolve(obj);
+ } else {
+ if (
+ getConfig()
+ .getFeatures()
+ .getActivationStrategy(BuiltInFeatures.OUTPUT_UNDEFINED_VARIABLES_ERROR)
+ .isActive(context)
+ ) {
+ addError(
+ new TemplateError(
+ ErrorType.WARNING,
+ ErrorReason.UNKNOWN,
+ ErrorItem.TOKEN,
+ "Undefined variable: '" + variable + "'",
+ null,
+ lineNumber,
+ startPosition,
+ null,
+ BasicTemplateErrorCategory.UNKNOWN,
+ ImmutableMap.of("variable", variable)
+ )
+ );
+ }
}
return obj;
}
@@ -537,7 +717,7 @@ public Object resolveObject(String variable, int lineNumber, int startPosition)
return "";
}
if (WhitespaceUtils.isQuoted(variable)) {
- return WhitespaceUtils.unquote(variable);
+ return WhitespaceUtils.unquoteAndUnescape(variable);
} else {
Object val = retraceVariable(variable, lineNumber, startPosition);
if (val == null) {
@@ -600,6 +780,17 @@ public JinjavaConfig getConfig() {
return config;
}
+ /**
+ * Resolve expression against current context, but does not add the expression to the set of resolved expressions.
+ *
+ * @param expression
+ * Jinja expression.
+ * @return Value of expression.
+ */
+ public Object resolveELExpressionSilently(String expression) {
+ return expressionResolver.resolveExpressionSilently(expression);
+ }
+
/**
* Resolve expression against current context.
*
@@ -685,46 +876,61 @@ public BlockInfo getCurrentBlock() {
}
public void addError(TemplateError templateError) {
- if (context.getThrowInterpreterErrors()) {
- if (templateError.getSeverity() == ErrorType.FATAL) {
- // Throw fatal errors when locating deferred words.
+ if (templateError == null) {
+ return;
+ }
+ ErrorHandlingStrategy errorHandlingStrategy = context.getErrorHandlingStrategy();
+ ErrorHandlingStrategy.TemplateErrorTypeHandlingStrategy errorTypeHandlingStrategy =
+ templateError.getSeverity() == ErrorType.FATAL
+ ? errorHandlingStrategy.getFatalErrorStrategy()
+ : errorHandlingStrategy.getNonFatalErrorStrategy();
+ switch (errorTypeHandlingStrategy) {
+ case IGNORE:
+ return;
+ case THROW_EXCEPTION:
throw new TemplateSyntaxException(
this,
templateError.getFieldName(),
templateError.getMessage()
);
- } else {
- // Hide warning errors when locating deferred words.
- return;
- }
- }
- // fix line numbers not matching up with source template
- if (!context.getCurrentPathStack().isEmpty()) {
- if (
- !templateError.getSourceTemplate().isPresent() &&
- context.getCurrentPathStack().peek().isPresent()
- ) {
- templateError.setMessage(
- getWrappedErrorMessage(
- context.getCurrentPathStack().peek().get(),
- templateError
- )
- );
- templateError.setSourceTemplate(context.getCurrentPathStack().peek().get());
- }
- templateError.setStartPosition(context.getCurrentPathStack().getTopStartPosition());
- templateError.setLineno(context.getCurrentPathStack().getTopLineNumber());
- }
+ case ADD_ERROR:
+ default: // Checkstyle
+ // fix line numbers not matching up with source template
+ if (!context.getCurrentPathStack().isEmpty()) {
+ if (
+ !templateError.getSourceTemplate().isPresent() &&
+ context.getCurrentPathStack().peek().isPresent()
+ ) {
+ templateError.setMessage(
+ getWrappedErrorMessage(
+ context.getCurrentPathStack().peek().get(),
+ templateError
+ )
+ );
+ templateError.setSourceTemplate(context.getCurrentPathStack().peek().get());
+ }
+ templateError.setStartPosition(
+ context.getCurrentPathStack().getTopStartPosition()
+ );
+ templateError.setLineno(context.getCurrentPathStack().getTopLineNumber());
+ }
- // Limit the number of error.
- if (errors.size() < MAX_ERROR_SIZE) {
- this.errors.add(templateError.withScopeDepth(scopeDepth));
+ // Limit the number of errors and filter duplicates
+ if (errors.size() < MAX_ERROR_SIZE) {
+ templateError = templateError.withScopeDepth(scopeDepth);
+ int errorCode = templateError.hashCode();
+ if (!errorSet.contains(errorCode)) {
+ this.errors.add(templateError);
+ this.errorSet.add(errorCode);
+ }
+ }
}
}
public void removeLastError() {
if (!errors.isEmpty()) {
- errors.remove(errors.size() - 1);
+ TemplateError error = errors.remove(errors.size() - 1);
+ errorSet.remove(error.hashCode());
}
}
@@ -760,17 +966,15 @@ public void addAllChildErrors(
childErrors
.stream()
.limit(MAX_ERROR_SIZE - errors.size())
- .forEach(
- error -> {
- if (!error.getSourceTemplate().isPresent()) {
- error.setMessage(getWrappedErrorMessage(childTemplateName, error));
- error.setSourceTemplate(childTemplateName);
- }
- error.setStartPosition(this.getPosition());
- error.setLineno(this.getLineNumber());
- this.addError(error);
+ .forEach(error -> {
+ if (!error.getSourceTemplate().isPresent()) {
+ error.setMessage(getWrappedErrorMessage(childTemplateName, error));
+ error.setSourceTemplate(childTemplateName);
}
- );
+ error.setStartPosition(this.getPosition());
+ error.setLineno(this.getLineNumber());
+ this.addError(error);
+ });
}
// We cannot just remove this, other projects may depend on it.
@@ -783,26 +987,35 @@ public List getErrorsCopy() {
return Lists.newArrayList(errors);
}
- private static final ThreadLocal> CURRENT_INTERPRETER = ThreadLocal.withInitial(
- Stack::new
- );
+ private static final ThreadLocal> CURRENT_INTERPRETER =
+ ThreadLocal.withInitial(Stack::new);
public static JinjavaInterpreter getCurrent() {
- if (CURRENT_INTERPRETER.get().isEmpty()) {
+ Stack stack = CURRENT_INTERPRETER.get();
+ if (stack.isEmpty()) {
return null;
}
-
- return CURRENT_INTERPRETER.get().peek();
+ return stack.peek();
}
public static Optional getCurrentMaybe() {
return Optional.ofNullable(getCurrent());
}
+ public static AutoCloseableSupplier closeablePushCurrent(
+ JinjavaInterpreter interpreter
+ ) {
+ Stack stack = CURRENT_INTERPRETER.get();
+ stack.push(interpreter);
+ return AutoCloseableSupplier.of(() -> interpreter, i -> stack.pop());
+ }
+
+ @Deprecated
public static void pushCurrent(JinjavaInterpreter interpreter) {
CURRENT_INTERPRETER.get().push(interpreter);
}
+ @Deprecated
public static void popCurrent() {
if (!CURRENT_INTERPRETER.get().isEmpty()) {
CURRENT_INTERPRETER.get().pop();
@@ -860,7 +1073,9 @@ private String getWrappedErrorMessage(
}
@Override
- public String toPyishString() {
- return ExtendedParser.INTERPRETER;
+ @SuppressWarnings("unchecked")
+ public T appendPyishString(T appendable)
+ throws IOException {
+ return (T) appendable.append(ExtendedParser.INTERPRETER);
}
}
diff --git a/src/main/java/com/hubspot/jinjava/interpret/LazyExpression.java b/src/main/java/com/hubspot/jinjava/interpret/LazyExpression.java
index 7ce13fc35..ee248f05a 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/LazyExpression.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/LazyExpression.java
@@ -4,6 +4,7 @@
import java.util.function.Supplier;
public class LazyExpression implements Supplier {
+
private final Supplier supplier;
private final String image;
private final Memoization memoization;
@@ -11,7 +12,7 @@ public class LazyExpression implements Supplier {
public enum Memoization {
ON,
- OFF
+ OFF,
}
protected LazyExpression(Supplier supplier, String image, Memoization memoization) {
diff --git a/src/main/java/com/hubspot/jinjava/interpret/LazyReference.java b/src/main/java/com/hubspot/jinjava/interpret/LazyReference.java
index 456b12997..06ffe2398 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/LazyReference.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/LazyReference.java
@@ -1,9 +1,11 @@
package com.hubspot.jinjava.interpret;
import com.hubspot.jinjava.objects.serialization.PyishSerializable;
+import java.io.IOException;
public class LazyReference extends LazyExpression implements PyishSerializable {
- private final String referenceKey;
+
+ private String referenceKey;
protected LazyReference(Context referenceContext, String referenceKey) {
super(() -> referenceContext.get(referenceKey), "", Memoization.ON);
@@ -19,8 +21,14 @@ public String getReferenceKey() {
return referenceKey;
}
+ public void setReferenceKey(String referenceKey) {
+ this.referenceKey = referenceKey;
+ }
+
@Override
- public String toPyishString() {
- return getReferenceKey();
+ @SuppressWarnings("unchecked")
+ public T appendPyishString(T appendable)
+ throws IOException {
+ return (T) appendable.append(getReferenceKey());
}
}
diff --git a/src/main/java/com/hubspot/jinjava/interpret/MacroTagCycleException.java b/src/main/java/com/hubspot/jinjava/interpret/MacroTagCycleException.java
index ef17412b5..f71bfb6a2 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/MacroTagCycleException.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/MacroTagCycleException.java
@@ -1,6 +1,7 @@
package com.hubspot.jinjava.interpret;
public class MacroTagCycleException extends TagCycleException {
+
private static final long serialVersionUID = -7552850581260771832L;
public MacroTagCycleException(String path, int lineNumber, int startPosition) {
diff --git a/src/main/java/com/hubspot/jinjava/interpret/MetaContextVariables.java b/src/main/java/com/hubspot/jinjava/interpret/MetaContextVariables.java
new file mode 100644
index 000000000..2630a41ac
--- /dev/null
+++ b/src/main/java/com/hubspot/jinjava/interpret/MetaContextVariables.java
@@ -0,0 +1,52 @@
+package com.hubspot.jinjava.interpret;
+
+import com.google.common.annotations.Beta;
+import java.util.Objects;
+
+@Beta
+public class MetaContextVariables {
+
+ public static final String TEMPORARY_META_CONTEXT_PREFIX = "__temp_meta_";
+ private static final String TEMPORARY_IMPORT_ALIAS_PREFIX =
+ TEMPORARY_META_CONTEXT_PREFIX + "import_alias_";
+
+ private static final String TEMPORARY_IMPORT_ALIAS_FORMAT =
+ TEMPORARY_IMPORT_ALIAS_PREFIX + "%d__";
+ private static final String TEMP_CURRENT_PATH_PREFIX =
+ TEMPORARY_META_CONTEXT_PREFIX + "current_path_";
+ private static final String TEMP_CURRENT_PATH_FORMAT =
+ TEMP_CURRENT_PATH_PREFIX + "%d__";
+
+ public static boolean isMetaContextVariable(String varName, Context context) {
+ if (isTemporaryMetaContextVariable(varName)) {
+ return true;
+ }
+ return (
+ context.getMetaContextVariables().contains(varName) &&
+ !context.getNonMetaContextVariables().contains(varName)
+ );
+ }
+
+ private static boolean isTemporaryMetaContextVariable(String varName) {
+ return varName.startsWith(TEMPORARY_META_CONTEXT_PREFIX);
+ }
+
+ public static boolean isTemporaryImportAlias(String varName) {
+ // This is just faster than checking a regex
+ return varName.startsWith(TEMPORARY_IMPORT_ALIAS_PREFIX);
+ }
+
+ public static String getTemporaryImportAlias(String fullAlias) {
+ return String.format(
+ TEMPORARY_IMPORT_ALIAS_FORMAT,
+ Math.abs(Objects.hashCode(fullAlias))
+ );
+ }
+
+ public static String getTemporaryCurrentPathVarName(String newPath) {
+ return String.format(
+ TEMP_CURRENT_PATH_FORMAT,
+ Math.abs(Objects.hash(newPath, TEMPORARY_META_CONTEXT_PREFIX) >> 1)
+ );
+ }
+}
diff --git a/src/main/java/com/hubspot/jinjava/interpret/MissingEndTagException.java b/src/main/java/com/hubspot/jinjava/interpret/MissingEndTagException.java
index 764c008fc..bdf154417 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/MissingEndTagException.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/MissingEndTagException.java
@@ -3,6 +3,7 @@
import org.apache.commons.lang3.StringUtils;
public class MissingEndTagException extends TemplateSyntaxException {
+
private static final long serialVersionUID = 1L;
private final String endTag;
diff --git a/src/main/java/com/hubspot/jinjava/interpret/NotInLoopException.java b/src/main/java/com/hubspot/jinjava/interpret/NotInLoopException.java
new file mode 100644
index 000000000..d4bd77eb9
--- /dev/null
+++ b/src/main/java/com/hubspot/jinjava/interpret/NotInLoopException.java
@@ -0,0 +1,14 @@
+package com.hubspot.jinjava.interpret;
+
+/**
+ * Exception thrown when `continue` or `break` is called outside of a loop
+ */
+public class NotInLoopException extends InterpretException {
+
+ public static final String MESSAGE_PREFIX = "`";
+ public static final String MESSAGE_SUFFIX = "` called while not in a for loop";
+
+ public NotInLoopException(String tagName) {
+ super(MESSAGE_PREFIX + tagName + MESSAGE_SUFFIX);
+ }
+}
diff --git a/src/main/java/com/hubspot/jinjava/interpret/NullValue.java b/src/main/java/com/hubspot/jinjava/interpret/NullValue.java
new file mode 100644
index 000000000..bd7f115ce
--- /dev/null
+++ b/src/main/java/com/hubspot/jinjava/interpret/NullValue.java
@@ -0,0 +1,50 @@
+package com.hubspot.jinjava.interpret;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.StdSerializer;
+import com.hubspot.jinjava.interpret.NullValue.NullValueSerializer;
+import java.io.IOException;
+
+/**
+ * Marker object of a `null` value. A null value in the map is usually considered
+ * the key does not exist. For example map = {"a": null}, if map.get("a") == null,
+ * we treat it as the there is not key "a" in the map.
+ */
+@JsonSerialize(using = NullValueSerializer.class)
+public final class NullValue {
+
+ public static final NullValue INSTANCE = new NullValue();
+
+ public static class NullValueSerializer extends StdSerializer {
+
+ public NullValueSerializer() {
+ this(null);
+ }
+
+ protected NullValueSerializer(Class t) {
+ super(t);
+ }
+
+ @Override
+ public void serialize(
+ NullValue value,
+ JsonGenerator jgen,
+ SerializerProvider provider
+ ) throws IOException {
+ jgen.writeNull();
+ }
+ }
+
+ private NullValue() {}
+
+ public static NullValue instance() {
+ return INSTANCE;
+ }
+
+ @Override
+ public String toString() {
+ return "null";
+ }
+}
diff --git a/src/main/java/com/hubspot/jinjava/interpret/OneTimeReconstructible.java b/src/main/java/com/hubspot/jinjava/interpret/OneTimeReconstructible.java
new file mode 100644
index 000000000..197a7050c
--- /dev/null
+++ b/src/main/java/com/hubspot/jinjava/interpret/OneTimeReconstructible.java
@@ -0,0 +1,7 @@
+package com.hubspot.jinjava.interpret;
+
+public interface OneTimeReconstructible extends DeferredValue {
+ boolean isReconstructed();
+
+ void setReconstructed(boolean reconstructed);
+}
diff --git a/src/main/java/com/hubspot/jinjava/interpret/OutputTooBigException.java b/src/main/java/com/hubspot/jinjava/interpret/OutputTooBigException.java
index 79ac4fe64..d13e59530 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/OutputTooBigException.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/OutputTooBigException.java
@@ -1,6 +1,7 @@
package com.hubspot.jinjava.interpret;
public class OutputTooBigException extends RuntimeException {
+
private long maxSize;
private final long size;
diff --git a/src/main/java/com/hubspot/jinjava/interpret/PartiallyDeferredValue.java b/src/main/java/com/hubspot/jinjava/interpret/PartiallyDeferredValue.java
index 4da2c3d12..73e355146 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/PartiallyDeferredValue.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/PartiallyDeferredValue.java
@@ -1,7 +1,10 @@
package com.hubspot.jinjava.interpret;
+import com.google.common.annotations.Beta;
+
/**
* An interface for a type of DeferredValue that as a whole is not deferred,
* but certain attributes or methods within it are deferred.
*/
+@Beta
public interface PartiallyDeferredValue extends DeferredValue {}
diff --git a/src/main/java/com/hubspot/jinjava/interpret/RenderResult.java b/src/main/java/com/hubspot/jinjava/interpret/RenderResult.java
index 7fd811ee1..4c2d1fd7a 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/RenderResult.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/RenderResult.java
@@ -6,6 +6,7 @@
import java.util.Optional;
public class RenderResult {
+
private final String output;
private final Context context;
private final List errors;
diff --git a/src/main/java/com/hubspot/jinjava/interpret/RevertibleObject.java b/src/main/java/com/hubspot/jinjava/interpret/RevertibleObject.java
index 1b4d894d4..acee72604 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/RevertibleObject.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/RevertibleObject.java
@@ -1,8 +1,11 @@
package com.hubspot.jinjava.interpret;
+import com.google.common.annotations.Beta;
import java.util.Optional;
+@Beta
public class RevertibleObject {
+
private final Object hashCode;
private final Optional pyishString;
diff --git a/src/main/java/com/hubspot/jinjava/interpret/TagCycleException.java b/src/main/java/com/hubspot/jinjava/interpret/TagCycleException.java
index 4128f52ff..a27248840 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/TagCycleException.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/TagCycleException.java
@@ -3,6 +3,7 @@
import java.util.Optional;
public class TagCycleException extends TemplateStateException {
+
private static final long serialVersionUID = -3058494056577268723L;
private final String path;
diff --git a/src/main/java/com/hubspot/jinjava/interpret/TemplateError.java b/src/main/java/com/hubspot/jinjava/interpret/TemplateError.java
index ff143aadb..495f617ed 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/TemplateError.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/TemplateError.java
@@ -10,6 +10,7 @@
import org.apache.commons.lang3.exception.ExceptionUtils;
public class TemplateError {
+
private static final Pattern GENERIC_TOSTRING_PATTERN = Pattern.compile(
"@[0-9a-z]{4,}$"
);
@@ -17,7 +18,7 @@ public class TemplateError {
public enum ErrorType {
FATAL,
- WARNING
+ WARNING,
}
public enum ErrorReason {
@@ -32,7 +33,7 @@ public enum ErrorReason {
OUTPUT_TOO_BIG,
OVER_LIMIT,
COLLECTION_TOO_BIG,
- OTHER
+ OTHER,
}
public enum ErrorItem {
@@ -43,7 +44,7 @@ public enum ErrorItem {
PROPERTY,
FILTER,
EXPRESSION_TEST,
- OTHER
+ OTHER,
}
private final ErrorType severity;
@@ -134,6 +135,19 @@ public static TemplateError fromInvalidInputException(InvalidInputException ex)
);
}
+ public static TemplateError fromMissingFilterArgException(InvalidArgumentException ex) {
+ return new TemplateError(
+ ErrorType.WARNING,
+ ErrorReason.INVALID_ARGUMENT,
+ ErrorItem.FILTER,
+ ex.getMessage(),
+ ex.getName(),
+ ex.getLineNumber(),
+ ex.getStartPosition(),
+ ex
+ );
+ }
+
public static TemplateError fromException(Exception ex) {
int lineNumber = -1;
int startPosition = -1;
@@ -540,7 +554,8 @@ public int hashCode() {
startPosition,
category,
categoryErrors,
- scopeDepth
+ scopeDepth,
+ sourceTemplate
);
}
}
diff --git a/src/main/java/com/hubspot/jinjava/interpret/TemplateStateException.java b/src/main/java/com/hubspot/jinjava/interpret/TemplateStateException.java
index 1e1b75bf5..e98aaeb32 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/TemplateStateException.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/TemplateStateException.java
@@ -1,6 +1,7 @@
package com.hubspot.jinjava.interpret;
public class TemplateStateException extends InterpretException {
+
private static final long serialVersionUID = 426925445445430522L;
public TemplateStateException(String msg) {
diff --git a/src/main/java/com/hubspot/jinjava/interpret/TemplateSyntaxException.java b/src/main/java/com/hubspot/jinjava/interpret/TemplateSyntaxException.java
index c31a22d23..1ac4ff731 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/TemplateSyntaxException.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/TemplateSyntaxException.java
@@ -1,6 +1,7 @@
package com.hubspot.jinjava.interpret;
public class TemplateSyntaxException extends InterpretException {
+
private static final long serialVersionUID = 1L;
private final String code;
diff --git a/src/main/java/com/hubspot/jinjava/interpret/UnexpectedTokenException.java b/src/main/java/com/hubspot/jinjava/interpret/UnexpectedTokenException.java
index 1e231471b..1d1ae33a3 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/UnexpectedTokenException.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/UnexpectedTokenException.java
@@ -1,6 +1,7 @@
package com.hubspot.jinjava.interpret;
public class UnexpectedTokenException extends TemplateSyntaxException {
+
private static final long serialVersionUID = 1L;
private final String token;
diff --git a/src/main/java/com/hubspot/jinjava/interpret/UnknownTagException.java b/src/main/java/com/hubspot/jinjava/interpret/UnknownTagException.java
index 216c86110..a07621ce7 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/UnknownTagException.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/UnknownTagException.java
@@ -3,6 +3,7 @@
import com.hubspot.jinjava.tree.parse.TagToken;
public class UnknownTagException extends TemplateSyntaxException {
+
private static final long serialVersionUID = 1L;
private final String tag;
diff --git a/src/main/java/com/hubspot/jinjava/interpret/UnknownTokenException.java b/src/main/java/com/hubspot/jinjava/interpret/UnknownTokenException.java
index 3e0425a1e..243473826 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/UnknownTokenException.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/UnknownTokenException.java
@@ -1,6 +1,7 @@
package com.hubspot.jinjava.interpret;
public class UnknownTokenException extends InterpretException {
+
private static final long serialVersionUID = -388757722051666198L;
private final String token;
diff --git a/src/main/java/com/hubspot/jinjava/interpret/errorcategory/BasicTemplateErrorCategory.java b/src/main/java/com/hubspot/jinjava/interpret/errorcategory/BasicTemplateErrorCategory.java
index ccfcaa912..e17932a44 100644
--- a/src/main/java/com/hubspot/jinjava/interpret/errorcategory/BasicTemplateErrorCategory.java
+++ b/src/main/java/com/hubspot/jinjava/interpret/errorcategory/BasicTemplateErrorCategory.java
@@ -8,5 +8,5 @@ public enum BasicTemplateErrorCategory implements TemplateErrorCategory {
UNKNOWN,
UNKNOWN_DATE,
UNKNOWN_LOCALE,
- UNKNOWN_PROPERTY
+ UNKNOWN_PROPERTY,
}
diff --git a/src/main/java/com/hubspot/jinjava/lib/SimpleLibrary.java b/src/main/java/com/hubspot/jinjava/lib/SimpleLibrary.java
index 5f6945a3d..93afea000 100644
--- a/src/main/java/com/hubspot/jinjava/lib/SimpleLibrary.java
+++ b/src/main/java/com/hubspot/jinjava/lib/SimpleLibrary.java
@@ -28,6 +28,7 @@
import java.util.stream.Collectors;
public abstract class SimpleLibrary {
+
private Map lib = new HashMap<>();
private Set disabled = new HashSet<>();
diff --git a/src/main/java/com/hubspot/jinjava/lib/expression/DefaultExpressionStrategy.java b/src/main/java/com/hubspot/jinjava/lib/expression/DefaultExpressionStrategy.java
index cc1c2b589..96140b745 100644
--- a/src/main/java/com/hubspot/jinjava/lib/expression/DefaultExpressionStrategy.java
+++ b/src/main/java/com/hubspot/jinjava/lib/expression/DefaultExpressionStrategy.java
@@ -1,5 +1,7 @@
package com.hubspot.jinjava.lib.expression;
+import com.hubspot.jinjava.features.BuiltInFeatures;
+import com.hubspot.jinjava.features.FeatureActivationStrategy;
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
import com.hubspot.jinjava.lib.filter.EscapeFilter;
import com.hubspot.jinjava.objects.SafeString;
@@ -9,7 +11,9 @@
import org.apache.commons.lang3.StringUtils;
public class DefaultExpressionStrategy implements ExpressionStrategy {
+
private static final long serialVersionUID = 436239440273704843L;
+ public static final String ECHO_UNDEFINED = BuiltInFeatures.ECHO_UNDEFINED;
public RenderedOutputNode interpretOutput(
ExpressionToken master,
@@ -19,15 +23,23 @@ public RenderedOutputNode interpretOutput(
master.getExpr(),
master.getLineNumber()
);
+
+ final FeatureActivationStrategy feat = interpreter
+ .getConfig()
+ .getFeatures()
+ .getActivationStrategy(BuiltInFeatures.ECHO_UNDEFINED);
+
+ if (var == null && feat.isActive(interpreter.getContext())) {
+ return new RenderedOutputNode(master.getImage());
+ }
+
String result = interpreter.getAsString(var);
if (interpreter.getConfig().isNestedInterpretationEnabled()) {
if (
!StringUtils.equals(result, master.getImage()) &&
- (
- StringUtils.contains(result, master.getSymbols().getExpressionStart()) ||
- StringUtils.contains(result, master.getSymbols().getExpressionStartWithTag())
- )
+ (StringUtils.contains(result, master.getSymbols().getExpressionStart()) ||
+ StringUtils.contains(result, master.getSymbols().getExpressionStartWithTag()))
) {
try {
result = interpreter.renderFlat(result);
diff --git a/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java b/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java
index f5f1c98aa..35a184f52 100644
--- a/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java
+++ b/src/main/java/com/hubspot/jinjava/lib/expression/EagerExpressionStrategy.java
@@ -1,24 +1,26 @@
package com.hubspot.jinjava.lib.expression;
+import com.google.common.annotations.Beta;
import com.hubspot.jinjava.JinjavaConfig;
-import com.hubspot.jinjava.interpret.DeferredMacroValueImpl;
+import com.hubspot.jinjava.interpret.Context.TemporaryValueClosable;
+import com.hubspot.jinjava.interpret.ErrorHandlingStrategy;
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
-import com.hubspot.jinjava.interpret.TemplateError.ErrorReason;
+import com.hubspot.jinjava.interpret.TemplateSyntaxException;
import com.hubspot.jinjava.lib.filter.EscapeFilter;
-import com.hubspot.jinjava.lib.tag.RawTag;
import com.hubspot.jinjava.lib.tag.eager.DeferredToken;
import com.hubspot.jinjava.lib.tag.eager.EagerExecutionResult;
import com.hubspot.jinjava.tree.output.RenderedOutputNode;
import com.hubspot.jinjava.tree.parse.ExpressionToken;
+import com.hubspot.jinjava.util.EagerContextWatcher;
import com.hubspot.jinjava.util.EagerExpressionResolver;
import com.hubspot.jinjava.util.EagerReconstructionUtils;
-import com.hubspot.jinjava.util.EagerReconstructionUtils.EagerChildContextConfig;
import com.hubspot.jinjava.util.Logging;
-import java.util.Objects;
-import java.util.stream.Collectors;
+import com.hubspot.jinjava.util.PrefixToPreserveState;
import org.apache.commons.lang3.StringUtils;
+@Beta
public class EagerExpressionStrategy implements ExpressionStrategy {
+
private static final long serialVersionUID = -6792345439237764193L;
@Override
@@ -34,25 +36,30 @@ private String eagerResolveExpression(
JinjavaInterpreter interpreter
) {
interpreter.getContext().checkNumberOfDeferredTokens();
- EagerExecutionResult eagerExecutionResult = EagerReconstructionUtils.executeInChildContext(
+ EagerExecutionResult eagerExecutionResult = EagerContextWatcher.executeInChildContext(
eagerInterpreter ->
EagerExpressionResolver.resolveExpression(master.getExpr(), interpreter),
interpreter,
- EagerChildContextConfig
+ EagerContextWatcher.EagerChildContextConfig
.newBuilder()
.withTakeNewValue(true)
.withPartialMacroEvaluation(
interpreter.getConfig().isNestedInterpretationEnabled()
)
- .withCheckForContextChanges(interpreter.getContext().isDeferredExecutionMode())
.build()
);
- StringBuilder prefixToPreserveState = new StringBuilder();
- if (interpreter.getContext().isDeferredExecutionMode()) {
- prefixToPreserveState.append(eagerExecutionResult.getPrefixToPreserveState());
+ PrefixToPreserveState prefixToPreserveState = new PrefixToPreserveState();
+ if (
+ !eagerExecutionResult.getResult().isFullyResolved() ||
+ interpreter.getContext().isDeferredExecutionMode()
+ ) {
+ prefixToPreserveState.putAll(eagerExecutionResult.getPrefixToPreserveState());
} else {
- interpreter.getContext().putAll(eagerExecutionResult.getSpeculativeBindings());
+ EagerReconstructionUtils.commitSpeculativeBindings(
+ interpreter,
+ eagerExecutionResult
+ );
}
if (eagerExecutionResult.getResult().isFullyResolved()) {
String result = eagerExecutionResult.getResult().toString(true);
@@ -60,40 +67,28 @@ private String eagerResolveExpression(
prefixToPreserveState.toString() + postProcessResult(master, result, interpreter)
);
}
- prefixToPreserveState.append(
- EagerReconstructionUtils.reconstructFromContextBeforeDeferring(
- eagerExecutionResult.getResult().getDeferredWords(),
- interpreter
- )
- );
- String helpers = wrapInExpression(
+
+ String deferredExpressionImage = wrapInExpression(
eagerExecutionResult.getResult().toString(),
interpreter
);
- interpreter
- .getContext()
- .handleDeferredToken(
- new DeferredToken(
- new ExpressionToken(
- helpers,
- master.getLineNumber(),
- master.getStartPosition(),
- master.getSymbols()
- ),
- eagerExecutionResult
- .getResult()
- .getDeferredWords()
- .stream()
- .filter(
- word ->
- !(interpreter.getContext().get(word) instanceof DeferredMacroValueImpl)
- )
- .collect(Collectors.toSet())
- )
- );
+ EagerReconstructionUtils.hydrateReconstructionFromContextBeforeDeferring(
+ prefixToPreserveState,
+ eagerExecutionResult.getResult().getDeferredWords(),
+ interpreter
+ );
+ prefixToPreserveState.withAllInFront(
+ EagerReconstructionUtils.handleDeferredTokenAndReconstructReferences(
+ interpreter,
+ DeferredToken
+ .builderFromImage(deferredExpressionImage, master)
+ .addUsedDeferredWords(eagerExecutionResult.getResult().getDeferredWords())
+ .build()
+ )
+ );
// There is only a preserving prefix because it couldn't be entirely evaluated.
return EagerReconstructionUtils.wrapInAutoEscapeIfNeeded(
- prefixToPreserveState.toString() + helpers,
+ prefixToPreserveState.toString() + deferredExpressionImage,
interpreter
);
}
@@ -105,26 +100,26 @@ public static String postProcessResult(
) {
if (
!StringUtils.equals(result, master.getImage()) &&
- (
- StringUtils.contains(result, master.getSymbols().getExpressionStart()) ||
- StringUtils.contains(result, master.getSymbols().getExpressionStartWithTag())
- )
+ (StringUtils.contains(result, master.getSymbols().getExpressionStart()) ||
+ StringUtils.contains(result, master.getSymbols().getExpressionStartWithTag()))
) {
if (interpreter.getConfig().isNestedInterpretationEnabled()) {
- long errorSizeStart = getParsingErrorsCount(interpreter);
-
- interpreter.parse(result);
-
- if (getParsingErrorsCount(interpreter) == errorSizeStart) {
+ try {
+ try (
+ TemporaryValueClosable c = interpreter
+ .getContext()
+ .withErrorHandlingStrategy(ErrorHandlingStrategy.throwAll())
+ ) {
+ interpreter.parse(result);
+ }
try {
result = interpreter.renderFlat(result);
} catch (Exception e) {
Logging.ENGINE_LOG.warn("Error rendering variable node result", e);
}
- }
+ } catch (TemplateSyntaxException ignored) {}
} else {
- // Possible macro/set tag in front of this one. Includes result
- result = wrapInRawOrExpressionIfNeeded(result, interpreter);
+ result = EagerReconstructionUtils.wrapInRawIfNeeded(result, interpreter);
}
}
@@ -134,37 +129,6 @@ public static String postProcessResult(
return result;
}
- private static long getParsingErrorsCount(JinjavaInterpreter interpreter) {
- return interpreter
- .getErrors()
- .stream()
- .filter(Objects::nonNull)
- .filter(
- error ->
- "Unclosed comment".equals(error.getMessage()) ||
- error.getReason() == ErrorReason.DISABLED
- )
- .count();
- }
-
- private static String wrapInRawOrExpressionIfNeeded(
- String output,
- JinjavaInterpreter interpreter
- ) {
- JinjavaConfig config = interpreter.getConfig();
- if (
- config.getExecutionMode().isPreserveRawTags() &&
- !interpreter.getContext().isUnwrapRawOverride() &&
- (
- output.contains(config.getTokenScannerSymbols().getExpressionStart()) ||
- output.contains(config.getTokenScannerSymbols().getExpressionStartWithTag())
- )
- ) {
- return EagerReconstructionUtils.wrapInTag(output, RawTag.TAG_NAME, interpreter);
- }
- return output;
- }
-
private static String wrapInExpression(String output, JinjavaInterpreter interpreter) {
JinjavaConfig config = interpreter.getConfig();
return String.format(
diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/CollectionExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/CollectionExpTest.java
index 05062b693..d2190bca7 100644
--- a/src/main/java/com/hubspot/jinjava/lib/exptest/CollectionExpTest.java
+++ b/src/main/java/com/hubspot/jinjava/lib/exptest/CollectionExpTest.java
@@ -4,6 +4,8 @@
import com.hubspot.jinjava.el.ext.CollectionMembershipOperator;
public abstract class CollectionExpTest implements ExpTest {
+
protected static final TruthyTypeConverter TYPE_CONVERTER = new TruthyTypeConverter();
- protected static final CollectionMembershipOperator COLLECTION_MEMBERSHIP_OPERATOR = new CollectionMembershipOperator();
+ protected static final CollectionMembershipOperator COLLECTION_MEMBERSHIP_OPERATOR =
+ new CollectionMembershipOperator();
}
diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsBooleanExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsBooleanExpTest.java
index 89b053481..daa4c5574 100644
--- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsBooleanExpTest.java
+++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsBooleanExpTest.java
@@ -13,7 +13,7 @@
code = "{% if true is boolean %}\n" +
" \n" +
"{% endif %}"
- )
+ ),
}
)
public class IsBooleanExpTest implements ExpTest {
diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsDefinedExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsDefinedExpTest.java
index ca4429803..3b850322f 100644
--- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsDefinedExpTest.java
+++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsDefinedExpTest.java
@@ -13,7 +13,7 @@
code = "{% if variable is defined %}\n" +
"\n" +
"{% endif %}"
- )
+ ),
}
)
public class IsDefinedExpTest implements ExpTest {
diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsDivisibleByExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsDivisibleByExpTest.java
index ffcd9f073..1beb4a0f8 100644
--- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsDivisibleByExpTest.java
+++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsDivisibleByExpTest.java
@@ -24,7 +24,7 @@
"{% else %}\n" +
" \n" +
"{% endif %}"
- )
+ ),
}
)
public class IsDivisibleByExpTest implements ExpTest {
diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsEqualToExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsEqualToExpTest.java
index a14c3220a..d3095b139 100644
--- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsEqualToExpTest.java
+++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsEqualToExpTest.java
@@ -18,7 +18,7 @@
type = "object",
desc = "Another object to check equality against",
required = true
- )
+ ),
},
snippets = {
@JinjavaSnippet(
@@ -29,10 +29,11 @@
@JinjavaSnippet(
desc = "Usage with the selectattr filter",
code = "{{ users|selectattr(\"email\", \"equalto\", \"foo@bar.invalid\") }}"
- )
+ ),
}
)
public class IsEqualToExpTest implements ExpTest {
+
private static final TypeConverter TYPE_CONVERTER = new TruthyTypeConverter();
@Override
diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsEvenExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsEvenExpTest.java
index de0fa2842..707ffb875 100644
--- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsEvenExpTest.java
+++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsEvenExpTest.java
@@ -15,7 +15,7 @@
"{% else %}\n" +
" \n" +
"{% endif %}"
- )
+ ),
}
)
public class IsEvenExpTest implements ExpTest {
diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsFalseExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsFalseExpTest.java
index 54b6711e3..e3c15fd0c 100644
--- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsFalseExpTest.java
+++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsFalseExpTest.java
@@ -13,7 +13,7 @@
code = "{% if false is false %}\n" +
" \n" +
"{% endif %}"
- )
+ ),
}
)
public class IsFalseExpTest implements ExpTest {
diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsFloatExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsFloatExpTest.java
index e77cf544e..7b0af240a 100644
--- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsFloatExpTest.java
+++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsFloatExpTest.java
@@ -14,7 +14,7 @@
code = "{% if num is float %}\n" +
" \n" +
"{% endif %}"
- )
+ ),
}
)
public class IsFloatExpTest implements ExpTest {
diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsGeTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsGeTest.java
index 7e9a9f260..dbcf6279b 100644
--- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsGeTest.java
+++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsGeTest.java
@@ -18,7 +18,7 @@
type = "object",
desc = "Another object to compare against",
required = true
- )
+ ),
},
snippets = {
@JinjavaSnippet(
@@ -29,10 +29,11 @@
@JinjavaSnippet(
desc = "Usage with the selectattr filter",
code = "{{ users|selectattr(\"num\", \"ge\", \"2\") }}"
- )
+ ),
}
)
public class IsGeTest implements ExpTest {
+
private static final TypeConverter TYPE_CONVERTER = new TruthyTypeConverter();
@Override
diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsGtTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsGtTest.java
index 0e24de3ec..c88dcd481 100644
--- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsGtTest.java
+++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsGtTest.java
@@ -18,7 +18,7 @@
type = "object",
desc = "Another object to compare against",
required = true
- )
+ ),
},
snippets = {
@JinjavaSnippet(
@@ -29,10 +29,11 @@
@JinjavaSnippet(
desc = "Usage with the selectattr filter",
code = "{{ users|selectattr(\"num\", \"gt\", \"2\") }}"
- )
+ ),
}
)
public class IsGtTest implements ExpTest {
+
private static final TypeConverter TYPE_CONVERTER = new TruthyTypeConverter();
@Override
diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsInExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsInExpTest.java
index 6b6c4fe8b..f40d2b37e 100644
--- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsInExpTest.java
+++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsInExpTest.java
@@ -21,7 +21,7 @@
snippets = {
@JinjavaSnippet(code = "{{ 2 is in [1, 2, 3] }}"),
@JinjavaSnippet(code = "{{ 'b' is in 'abc' }}"),
- @JinjavaSnippet(code = "{{ 'k2' is in {'k1':'v1', 'k2':'v2'} }}")
+ @JinjavaSnippet(code = "{{ 'k2' is in {'k1':'v1', 'k2':'v2'} }}"),
}
)
public class IsInExpTest extends CollectionExpTest {
diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsIntegerExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsIntegerExpTest.java
index 79f646ee4..1500abad3 100644
--- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsIntegerExpTest.java
+++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsIntegerExpTest.java
@@ -15,7 +15,7 @@
code = "{% if num is integer %}\n" +
" \n" +
"{% endif %}"
- )
+ ),
}
)
public class IsIntegerExpTest implements ExpTest {
diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsIterableExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsIterableExpTest.java
index 9c2bb2b85..61b34face 100644
--- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsIterableExpTest.java
+++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsIterableExpTest.java
@@ -13,7 +13,7 @@
code = "{% if variable is iterable %}\n" +
" \n" +
"{% endif %}"
- )
+ ),
}
)
public class IsIterableExpTest implements ExpTest {
diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsLeTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsLeTest.java
index 956f3a2b5..e1423cf10 100644
--- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsLeTest.java
+++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsLeTest.java
@@ -18,7 +18,7 @@
type = "object",
desc = "Another object to compare against",
required = true
- )
+ ),
},
snippets = {
@JinjavaSnippet(
@@ -29,10 +29,11 @@
@JinjavaSnippet(
desc = "Usage with the selectattr filter",
code = "{{ users|selectattr(\"num\", \"le\", \"2\") }}"
- )
+ ),
}
)
public class IsLeTest implements ExpTest {
+
private static final TypeConverter TYPE_CONVERTER = new TruthyTypeConverter();
@Override
diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsLessThanOrEqualToSymbolExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsLessThanOrEqualToSymbolExpTest.java
index b3bb1e148..b9c19a5c5 100644
--- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsLessThanOrEqualToSymbolExpTest.java
+++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsLessThanOrEqualToSymbolExpTest.java
@@ -3,7 +3,7 @@
import com.hubspot.jinjava.doc.annotations.JinjavaDoc;
@JinjavaDoc(value = "", aliasOf = "le")
-public class IsLessThanOrEqualToSymbolExpTest extends IsLtTest {
+public class IsLessThanOrEqualToSymbolExpTest extends IsLeTest {
@Override
public String getName() {
diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsLowerExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsLowerExpTest.java
index c9be8e4ef..810067a49 100644
--- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsLowerExpTest.java
+++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsLowerExpTest.java
@@ -14,7 +14,7 @@
code = "{% if variable is lower %}\n" +
" \n" +
"{% endif %}"
- )
+ ),
}
)
public class IsLowerExpTest implements ExpTest {
diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsLtTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsLtTest.java
index afea491f3..ebcc184ce 100644
--- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsLtTest.java
+++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsLtTest.java
@@ -18,7 +18,7 @@
type = "object",
desc = "Another object to compare against",
required = true
- )
+ ),
},
snippets = {
@JinjavaSnippet(
@@ -29,10 +29,11 @@
@JinjavaSnippet(
desc = "Usage with the selectattr filter",
code = "{{ users|selectattr(\"num\", \"lt\", \"2\") }}"
- )
+ ),
}
)
public class IsLtTest implements ExpTest {
+
private static final TypeConverter TYPE_CONVERTER = new TruthyTypeConverter();
@Override
diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsMappingExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsMappingExpTest.java
index cb0c415f2..5cd697544 100644
--- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsMappingExpTest.java
+++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsMappingExpTest.java
@@ -14,7 +14,7 @@
code = "{% if variable is mapping %}\n" +
" \n" +
"{% endif %}"
- )
+ ),
}
)
public class IsMappingExpTest implements ExpTest {
diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsNeExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsNeExpTest.java
index 311a4e192..057382094 100644
--- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsNeExpTest.java
+++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsNeExpTest.java
@@ -18,7 +18,7 @@
type = "object",
desc = "Another object to check inequality against",
required = true
- )
+ ),
},
snippets = {
@JinjavaSnippet(
@@ -29,10 +29,11 @@
@JinjavaSnippet(
desc = "Usage with the selectattr filter",
code = "{{ users|selectattr(\"email\", \"ne\", \"foo@bar.invalid\") }}"
- )
+ ),
}
)
public class IsNeExpTest implements ExpTest {
+
private static final TypeConverter TYPE_CONVERTER = new TruthyTypeConverter();
@Override
diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsNoneExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsNoneExpTest.java
index 87a2b2683..93ff43f1e 100644
--- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsNoneExpTest.java
+++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsNoneExpTest.java
@@ -13,7 +13,7 @@
code = "{% unless variable is none %}\n" +
" \n" +
"{% endunless %}"
- )
+ ),
}
)
public class IsNoneExpTest implements ExpTest {
diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsNumberExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsNumberExpTest.java
index 9d03e6133..a024a8c06 100644
--- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsNumberExpTest.java
+++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsNumberExpTest.java
@@ -15,7 +15,7 @@
"{% else %}\n" +
" The variable is not a number.\n" +
"{% endif %}"
- )
+ ),
}
)
public class IsNumberExpTest implements ExpTest {
diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsOddExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsOddExpTest.java
index c22de8edb..287d15374 100644
--- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsOddExpTest.java
+++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsOddExpTest.java
@@ -15,7 +15,7 @@
"{% else %}\n" +
" \n" +
"{% endif %}"
- )
+ ),
}
)
public class IsOddExpTest implements ExpTest {
diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsSameAsExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsSameAsExpTest.java
index 1c13627a9..c357210d0 100644
--- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsSameAsExpTest.java
+++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsSameAsExpTest.java
@@ -20,7 +20,7 @@
code = "{% if var_one is sameas var_two %}\n" +
" \n" +
"{% endif %}"
- )
+ ),
}
)
public class IsSameAsExpTest implements ExpTest {
diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsSequenceExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsSequenceExpTest.java
index e2e2cfa53..c48c5e133 100644
--- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsSequenceExpTest.java
+++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsSequenceExpTest.java
@@ -15,7 +15,7 @@
code = "{% if variable is sequence %}\n" +
" \n" +
"{% endif %}"
- )
+ ),
}
)
public class IsSequenceExpTest implements ExpTest {
diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsStringContainingExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsStringContainingExpTest.java
index c5fa1fd2b..01d17d707 100644
--- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsStringContainingExpTest.java
+++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsStringContainingExpTest.java
@@ -20,7 +20,7 @@
code = "{% if variable is string_containing 'foo' %}\n" +
" \n" +
"{% endif %}"
- )
+ ),
}
)
public class IsStringContainingExpTest extends IsStringExpTest {
diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsStringExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsStringExpTest.java
index 47a49c3ab..b9cb56cdb 100644
--- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsStringExpTest.java
+++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsStringExpTest.java
@@ -14,7 +14,7 @@
code = "{% if variable is string %}\n" +
" \n" +
"{% endif %}"
- )
+ ),
}
)
public class IsStringExpTest implements ExpTest {
diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsStringStartingWithExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsStringStartingWithExpTest.java
index 398115035..8a7b527c0 100644
--- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsStringStartingWithExpTest.java
+++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsStringStartingWithExpTest.java
@@ -20,7 +20,7 @@
code = "{% if variable is string_startingwith 'foo' %}\n" +
" \n" +
"{% endif %}"
- )
+ ),
}
)
public class IsStringStartingWithExpTest extends IsStringExpTest {
diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsTrueExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsTrueExpTest.java
index c1c5407d3..31338ee4e 100644
--- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsTrueExpTest.java
+++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsTrueExpTest.java
@@ -13,7 +13,7 @@
code = "{% if false is true %}\n" +
" \n" +
"{% endif %}"
- )
+ ),
}
)
public class IsTrueExpTest implements ExpTest {
diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsTruthyExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsTruthyExpTest.java
index 911dbb816..8a7d09875 100644
--- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsTruthyExpTest.java
+++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsTruthyExpTest.java
@@ -14,7 +14,7 @@
code = "{% if variable is truthy %}\n" +
" \n" +
"{% endif %}"
- )
+ ),
}
)
public class IsTruthyExpTest implements ExpTest {
diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsUndefinedExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsUndefinedExpTest.java
index 13c481cbb..f1cb78e4b 100644
--- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsUndefinedExpTest.java
+++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsUndefinedExpTest.java
@@ -13,7 +13,7 @@
code = "{% if variable is undefined %}\n" +
" \n" +
"{% endif %}"
- )
+ ),
}
)
public class IsUndefinedExpTest implements ExpTest {
diff --git a/src/main/java/com/hubspot/jinjava/lib/exptest/IsUpperExpTest.java b/src/main/java/com/hubspot/jinjava/lib/exptest/IsUpperExpTest.java
index e7ca084e1..61d8246fe 100644
--- a/src/main/java/com/hubspot/jinjava/lib/exptest/IsUpperExpTest.java
+++ b/src/main/java/com/hubspot/jinjava/lib/exptest/IsUpperExpTest.java
@@ -15,7 +15,7 @@
code = "{% if variable is upper %}\n" +
" \n" +
"{% endif %}"
- )
+ ),
}
)
public class IsUpperExpTest implements ExpTest {
diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/AbsFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/AbsFilter.java
index ea860b024..578a92407 100644
--- a/src/main/java/com/hubspot/jinjava/lib/filter/AbsFilter.java
+++ b/src/main/java/com/hubspot/jinjava/lib/filter/AbsFilter.java
@@ -33,7 +33,7 @@
required = true
),
snippets = {
- @JinjavaSnippet(code = "{% set my_number = -53 %}\n" + "{{ my_number|abs }}")
+ @JinjavaSnippet(code = "{% set my_number = -53 %}\n" + "{{ my_number|abs }}"),
}
)
public class AbsFilter implements Filter {
diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/AbstractFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/AbstractFilter.java
index 978ce559a..b39344c05 100644
--- a/src/main/java/com/hubspot/jinjava/lib/filter/AbstractFilter.java
+++ b/src/main/java/com/hubspot/jinjava/lib/filter/AbstractFilter.java
@@ -21,7 +21,6 @@
import com.hubspot.jinjava.doc.annotations.JinjavaParam;
import com.hubspot.jinjava.interpret.InvalidInputException;
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
@@ -43,8 +42,11 @@
* @see JinjavaParam
*/
public abstract class AbstractFilter implements Filter {
- private static final Map> NAMED_ARGUMENTS_CACHE = new ConcurrentHashMap<>();
- private static final Map> DEFAULT_VALUES_CACHE = new ConcurrentHashMap<>();
+
+ private static final Map> NAMED_ARGUMENTS_CACHE =
+ new ConcurrentHashMap<>();
+ private static final Map> DEFAULT_VALUES_CACHE =
+ new ConcurrentHashMap<>();
private final Map namedArguments;
private final Map defaultValues;
@@ -66,10 +68,6 @@ public Object filter(Object var, JinjavaInterpreter interpreter, String... args)
return filter(var, interpreter, args, Collections.emptyMap());
}
- @SuppressFBWarnings(
- value = "UC_USELESS_OBJECT",
- justification = "FB bug prevents forEach() method call counting `namedArgs` as used (fixed in next release)"
- )
public Object filter(
Object var,
JinjavaInterpreter interpreter,
@@ -114,8 +112,8 @@ public Object filter(
//Parse args based on their declared types
Map parsedArgs = new HashMap<>();
- namedArgs.forEach(
- (k, v) -> parsedArgs.put(k, parseArg(interpreter, namedArguments.get(k), v))
+ namedArgs.forEach((k, v) ->
+ parsedArgs.put(k, parseArg(interpreter, namedArguments.get(k), v))
);
validateArgs(interpreter, parsedArgs);
@@ -204,9 +202,8 @@ public String getIndexedArgumentName(int position) {
.ofNullable(namedArguments)
.map(Map::keySet)
.map(ArrayList::new)
- .flatMap(
- argNames ->
- Optional.ofNullable(argNames.size() > position ? argNames.get(position) : null)
+ .flatMap(argNames ->
+ Optional.ofNullable(argNames.size() > position ? argNames.get(position) : null)
)
.orElse(null);
}
@@ -220,12 +217,13 @@ public Map initNamedArguments() {
}
if (jinjavaDoc != null) {
- ImmutableMap.Builder namedArgsBuilder = ImmutableMap.builder();
+ ImmutableMap.Builder namedArgsBuilder =
+ ImmutableMap.builder();
Arrays
.stream(jinjavaDoc.params())
- .forEachOrdered(
- jinjavaParam -> namedArgsBuilder.put(jinjavaParam.value(), jinjavaParam)
+ .forEachOrdered(jinjavaParam ->
+ namedArgsBuilder.put(jinjavaParam.value(), jinjavaParam)
);
return namedArgsBuilder.build();
diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/AbstractSetFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/AbstractSetFilter.java
index 42ae7e6ca..664f82760 100644
--- a/src/main/java/com/hubspot/jinjava/lib/filter/AbstractSetFilter.java
+++ b/src/main/java/com/hubspot/jinjava/lib/filter/AbstractSetFilter.java
@@ -1,10 +1,14 @@
package com.hubspot.jinjava.lib.filter;
+import com.hubspot.jinjava.features.BuiltInFeatures;
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
+import com.hubspot.jinjava.interpret.TemplateError;
import com.hubspot.jinjava.interpret.TemplateSyntaxException;
+import com.hubspot.jinjava.lib.fn.TypeFunction;
import com.hubspot.jinjava.util.ForLoop;
import com.hubspot.jinjava.util.ObjectIterator;
import java.util.LinkedHashSet;
+import java.util.Map;
import java.util.Set;
public abstract class AbstractSetFilter implements AdvancedFilter {
@@ -29,4 +33,112 @@ protected Set