diff --git a/src/main/java/com/hubspot/jinjava/el/ext/BeanELResolver.java b/src/main/java/com/hubspot/jinjava/el/ext/BeanELResolver.java new file mode 100644 index 000000000..90c9b60d6 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/el/ext/BeanELResolver.java @@ -0,0 +1,725 @@ +/* + * Copyright 2006-2009 Odysseus Software GmbH + * Modifications Copyright (c) 2023 HubSpot Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hubspot.jinjava.el.ext; + +import java.beans.FeatureDescriptor; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Array; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import javax.el.CompositeELResolver; +import javax.el.ELContext; +import javax.el.ELException; +import javax.el.ELResolver; +import javax.el.ExpressionFactory; +import javax.el.MethodNotFoundException; +import javax.el.PropertyNotFoundException; +import javax.el.PropertyNotWritableException; + +/** + * Defines property resolution behavior on objects using the JavaBeans component architecture. This + * resolver handles base objects of any type, as long as the base is not null. It accepts any object + * as a property, and coerces it to a string. That string is then used to find a JavaBeans compliant + * property on the base object. The value is accessed using JavaBeans getters and setters. This + * resolver can be constructed in read-only mode, which means that isReadOnly will always return + * true and {@link #setValue(ELContext, Object, Object, Object)} will always throw + * PropertyNotWritableException. ELResolvers are combined together using {@link CompositeELResolver} + * s, to define rich semantics for evaluating an expression. See the javadocs for {@link ELResolver} + * for details. Because this resolver handles base objects of any type, it should be placed near the + * end of a composite resolver. Otherwise, it will claim to have resolved a property before any + * resolvers that come after it get a chance to test if they can do so as well. + * + * @see CompositeELResolver + * @see ELResolver + */ +public class BeanELResolver extends ELResolver { + + protected static final class BeanProperties { + private final Map map = new HashMap(); + + public BeanProperties(Class baseClass) { + PropertyDescriptor[] descriptors; + try { + descriptors = Introspector.getBeanInfo(baseClass).getPropertyDescriptors(); + } catch (IntrospectionException e) { + throw new ELException(e); + } + for (PropertyDescriptor descriptor : descriptors) { + map.put(descriptor.getName(), new BeanProperty(descriptor)); + } + } + + public BeanProperty getBeanProperty(String property) { + return map.get(property); + } + } + + protected static final class BeanProperty { + private final PropertyDescriptor descriptor; + + private Method readMethod; + private Method writedMethod; + + public BeanProperty(PropertyDescriptor descriptor) { + this.descriptor = descriptor; + } + + public Class getPropertyType() { + return descriptor.getPropertyType(); + } + + public Method getReadMethod() { + if (readMethod == null) { + readMethod = findAccessibleMethod(descriptor.getReadMethod()); + } + return readMethod; + } + + public Method getWriteMethod() { + if (writedMethod == null) { + writedMethod = findAccessibleMethod(descriptor.getWriteMethod()); + } + return writedMethod; + } + + public boolean isReadOnly() { + return getWriteMethod() == null; + } + } + + private static Method findPublicAccessibleMethod(Method method) { + if (method == null || !Modifier.isPublic(method.getModifiers())) { + return null; + } + if ( + method.isAccessible() || + Modifier.isPublic(method.getDeclaringClass().getModifiers()) + ) { + return method; + } + for (Class cls : method.getDeclaringClass().getInterfaces()) { + Method mth = null; + try { + mth = + findPublicAccessibleMethod( + cls.getMethod(method.getName(), method.getParameterTypes()) + ); + if (mth != null) { + return mth; + } + } catch (NoSuchMethodException ignore) { + // do nothing + } + } + Class cls = method.getDeclaringClass().getSuperclass(); + if (cls != null) { + Method mth = null; + try { + mth = + findPublicAccessibleMethod( + cls.getMethod(method.getName(), method.getParameterTypes()) + ); + if (mth != null) { + return mth; + } + } catch (NoSuchMethodException ignore) { + // do nothing + } + } + return null; + } + + // Changed modifier to protected + protected static Method findAccessibleMethod(Method method) { + Method result = findPublicAccessibleMethod(method); + if (result == null && method != null && Modifier.isPublic(method.getModifiers())) { + result = method; + try { + method.setAccessible(true); + } catch (SecurityException e) { + result = null; + } + } + return result; + } + + private final boolean readOnly; + private final ConcurrentHashMap, BeanProperties> cache; + + private ExpressionFactory defaultFactory; + + /** + * Creates a new read/write BeanELResolver. + */ + public BeanELResolver() { + this(false); + } + + /** + * Creates a new BeanELResolver whose read-only status is determined by the given parameter. + */ + public BeanELResolver(boolean readOnly) { + this.readOnly = readOnly; + this.cache = new ConcurrentHashMap, BeanProperties>(); + } + + /** + * If the base object is not null, returns the most general type that this resolver accepts for + * the property argument. Otherwise, returns null. Assuming the base is not null, this method + * will always return Object.class. This is because any object is accepted as a key and is + * coerced into a string. + * + * @param context + * The context of this evaluation. + * @param base + * The bean to analyze. + * @return null if base is null; otherwise Object.class. + */ + @Override + public Class getCommonPropertyType(ELContext context, Object base) { + return isResolvable(base) ? Object.class : null; + } + + /** + * If the base object is not null, returns an Iterator containing the set of JavaBeans + * properties available on the given object. Otherwise, returns null. The Iterator returned must + * contain zero or more instances of java.beans.FeatureDescriptor. Each info object contains + * information about a property in the bean, as obtained by calling the + * BeanInfo.getPropertyDescriptors method. The FeatureDescriptor is initialized using the same + * fields as are present in the PropertyDescriptor, with the additional required named + * attributes "type" and "resolvableAtDesignTime" set as follows: + *
    + *
  • {@link ELResolver#TYPE} - The runtime type of the property, from + * PropertyDescriptor.getPropertyType().
  • + *
  • {@link ELResolver#RESOLVABLE_AT_DESIGN_TIME} - true.
  • + *
+ * + * @param context + * The context of this evaluation. + * @param base + * The bean to analyze. + * @return An Iterator containing zero or more FeatureDescriptor objects, each representing a + * property on this bean, or null if the base object is null. + */ + @Override + public Iterator getFeatureDescriptors( + ELContext context, + Object base + ) { + if (isResolvable(base)) { + final PropertyDescriptor[] properties; + try { + properties = Introspector.getBeanInfo(base.getClass()).getPropertyDescriptors(); + } catch (IntrospectionException e) { + return Collections.emptyList().iterator(); + } + return new Iterator() { + 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 = toBeanProperty(base, property).getReadMethod(); + 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 = toBeanProperty(base, property).getWriteMethod(); + 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; + } + + private 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 new PropertyNotFoundException( + "Could not find property " + property + " in " + base.getClass() + ); + } + 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/JinjavaBeanELResolver.java b/src/main/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolver.java index 75a140aa2..cb0cc46a8 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolver.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolver.java @@ -5,11 +5,13 @@ import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.util.EagerReconstructionUtils; +import java.lang.invoke.MethodType; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.util.LinkedList; +import java.util.List; import java.util.Set; -import javax.el.BeanELResolver; import javax.el.ELContext; import javax.el.MethodNotFoundException; @@ -134,6 +136,78 @@ public Object invoke( return result; } + @Override + protected Method findMethod( + Object base, + String name, + Class[] types, + Object[] params, + int paramCount + ) { + if (types != null) { + return super.findMethod(base, name, types, params, paramCount); + } + Method varArgsMethod = null; + + Method[] methods = base.getClass().getMethods(); + List potentialMethods = new LinkedList<>(); + + for (Method method : methods) { + if (method.getName().equals(name)) { + int formalParamCount = method.getParameterTypes().length; + if (method.isVarArgs() && paramCount >= formalParamCount - 1) { + varArgsMethod = method; + } else if (paramCount == formalParamCount) { + potentialMethods.add(findAccessibleMethod(method)); + } + } + } + final Method finalVarArgsMethod = varArgsMethod; + return potentialMethods + .stream() + .filter(method -> checkAssignableParameterTypes(params, method)) + .min(JinjavaBeanELResolver::pickMoreSpecificMethod) + .orElseGet( + () -> + potentialMethods + .stream() + .findAny() + .orElseGet( + () -> + finalVarArgsMethod == null + ? null + : findAccessibleMethod(finalVarArgsMethod) + ) + ); + } + + 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; + } + + 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; + } + private String validatePropertyName(Object property) { String propertyName = transformPropertyName(property); diff --git a/src/test/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolverTest.java b/src/test/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolverTest.java new file mode 100644 index 000000000..f9ba4f281 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolverTest.java @@ -0,0 +1,145 @@ +package com.hubspot.jinjava.el.ext; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.hubspot.jinjava.el.JinjavaELContext; +import javax.el.ELContext; +import org.junit.Before; +import org.junit.Test; + +public class JinjavaBeanELResolverTest { + private JinjavaBeanELResolver jinjavaBeanELResolver; + private ELContext elContext; + + @Before + public void setUp() throws Exception { + jinjavaBeanELResolver = new JinjavaBeanELResolver(); + elContext = new JinjavaELContext(); + } + + @Test + public void itInvokesProperStringReplace() { + assertThat( + jinjavaBeanELResolver.invoke( + elContext, + "abcd", + "replace", + null, + new Object[] { "abcd", "efgh" } + ) + ) + .isEqualTo("efgh"); + assertThat( + jinjavaBeanELResolver.invoke( + elContext, + "abcd", + "replace", + null, + new Object[] { 'a', 'e' } + ) + ) + .isEqualTo("ebcd"); + } + + @Test + public void itInvokesBestMethodWithSingleParam() { + class Temp { + + public String getResult(int a) { + return "int"; + } + + public String getResult(String a) { + return "String"; + } + + public String getResult(Object a) { + return "Object"; + } + + public String getResult(CharSequence a) { + return "CharSequence"; + } + } + Temp var = new Temp(); + assertThat( + jinjavaBeanELResolver.invoke( + elContext, + var, + "getResult", + null, + new Object[] { 1 } + ) + ) + .isEqualTo("int"); + assertThat( + jinjavaBeanELResolver.invoke( + elContext, + var, + "getResult", + null, + new Object[] { "1" } + ) + ) + .isEqualTo("String"); + assertThat( + jinjavaBeanELResolver.invoke( + elContext, + var, + "getResult", + null, + new Object[] { new Object() } + ) + ) + .isEqualTo("Object"); + } + + @Test + public void itPrefersPrimitives() { + class Temp { + + public String getResult(int a, Integer b) { + return "int Integer"; + } + + public String getResult(int a, Object b) { + return "int Object"; + } + + public String getResult(Number a, int b) { + return "Number int"; + } + } + Temp var = new Temp(); + assertThat( + jinjavaBeanELResolver.invoke( + elContext, + var, + "getResult", + null, + new Object[] { 1, 2 } + ) + ) + .isEqualTo("int Integer"); + assertThat( + jinjavaBeanELResolver.invoke( + elContext, + var, + "getResult", + null, + new Object[] { 1, Integer.valueOf(2) } + ) + ) + .isEqualTo("int Integer"); // should be "int object", but we can't figure that out + assertThat( + jinjavaBeanELResolver.invoke( + elContext, + var, + "getResult", + null, + new Object[] { Integer.valueOf(1), 2 } + ) + ) + .isEqualTo("int Integer"); // should be "Number int", but we can't figure that out + } +}