blob: dfcee43ca125aa4c4374c5ec17340d6372a77c3e [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.el.lang;
import java.beans.PropertyEditor;
import java.beans.PropertyEditorManager;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Timestamp;
import java.time.Clock;
import java.time.Instant;
import java.time.temporal.TemporalAccessor;
import java.util.Collections;
import java.util.Date;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier;
import jakarta.el.ELContext;
import jakarta.el.ELException;
import jakarta.el.LambdaExpression;
import org.apache.el.util.ExceptionUtils;
import org.apache.el.util.MessageFactory;
/**
* A helper class that implements the EL Specification.
*/
public class ELSupport {
private static final Long ZERO = Long.valueOf(0L);
protected static final boolean COERCE_TO_ZERO = Boolean.getBoolean("org.apache.el.parser.COERCE_TO_ZERO");
/**
* Compare two objects, after coercing to the same type if appropriate.
* <p>
* If the objects are identical, or they are equal according to {@link #equals(ELContext, Object, Object)} then
* return 0.
* <p>
* If either object is null, error
* <p>
* If either object is a BigDecimal, then coerce both to BigDecimal first. Similarly for Double(Float), BigInteger,
* and Long(Integer, Char, Short, Byte).
* <p>
* If either object is TemporalAccessor, Clock, java.util.Date or java.sql.Timestamp, coerce both to Instant and
* then compare.
* <p>
* If the first object is an instance of Comparable, return the result of comparing against the second object.
* <p>
* If the second object is an instance of Comparable, return -1 * the result of comparing against the first object.
* <p>
* Otherwise, error.
*
* @param ctx the context in which this comparison is taking place
* @param obj0 first object
* @param obj1 second object
*
* @return -1, 0, or 1 if this object is less than, equal to, or greater than val.
*
* @throws ELException if neither object is Comparable
* @throws ClassCastException if the objects are not mutually comparable
*/
public static int compare(final ELContext ctx, final Object obj0, final Object obj1) throws ELException {
if (obj0 == obj1 || equals(ctx, obj0, obj1)) {
return 0;
}
Objects.requireNonNull(obj0, MessageFactory.get("error.compare.null"));
Objects.requireNonNull(obj1, MessageFactory.get("error.compare.null"));
if (isBigDecimalOp(obj0, obj1)) {
BigDecimal bd0 = (BigDecimal) coerceToNumber(ctx, obj0, BigDecimal.class);
BigDecimal bd1 = (BigDecimal) coerceToNumber(ctx, obj1, BigDecimal.class);
return bd0.compareTo(bd1);
}
if (isDoubleOp(obj0, obj1)) {
Double d0 = (Double) coerceToNumber(ctx, obj0, Double.class);
Double d1 = (Double) coerceToNumber(ctx, obj1, Double.class);
return d0.compareTo(d1);
}
if (isBigIntegerOp(obj0, obj1)) {
BigInteger bi0 = (BigInteger) coerceToNumber(ctx, obj0, BigInteger.class);
BigInteger bi1 = (BigInteger) coerceToNumber(ctx, obj1, BigInteger.class);
return bi0.compareTo(bi1);
}
if (isLongOp(obj0, obj1)) {
Long l0 = (Long) coerceToNumber(ctx, obj0, Long.class);
Long l1 = (Long) coerceToNumber(ctx, obj1, Long.class);
return l0.compareTo(l1);
}
if (isDateOp(obj0, obj1)) {
Instant i0 = coerceToInstant(ctx, obj0);
Instant i1 = coerceToInstant(ctx, obj1);
return i0.compareTo(i1);
}
if (obj0 instanceof String || obj1 instanceof String) {
return coerceToString(ctx, obj0).compareTo(coerceToString(ctx, obj1));
}
if (obj0 instanceof Comparable<?>) {
@SuppressWarnings("unchecked") // checked above
final Comparable<Object> comparable = (Comparable<Object>) obj0;
return comparable.compareTo(obj1);
}
if (obj1 instanceof Comparable<?>) {
@SuppressWarnings("unchecked") // checked above
final Comparable<Object> comparable = (Comparable<Object>) obj1;
return -comparable.compareTo(obj0);
}
throw new ELException(MessageFactory.get("error.compare", obj0, obj1));
}
/**
* Compare two objects for equality, after coercing to the same type if appropriate.
* <p>
* If the objects are identical (including both null) return true.
* <p>
* If either object is null, return false.
* <p>
* If either object is Boolean, coerce both to Boolean and check equality.
* <p>
* Similarly for Enum, String, BigDecimal, Double(Float), Long(Integer, Short, Byte, Character)
* <p>
* Otherwise default to using Object.equals().
*
* @param ctx the context in which this equality test is taking place
* @param obj0 the first object
* @param obj1 the second object
*
* @return true if the objects are equal
*
* @throws ELException if one of the coercion fails
*/
public static boolean equals(final ELContext ctx, final Object obj0, final Object obj1) throws ELException {
if (obj0 == obj1) {
return true;
} else if (obj0 == null || obj1 == null) {
return false;
} else if (isBigDecimalOp(obj0, obj1)) {
BigDecimal bd0 = (BigDecimal) coerceToNumber(ctx, obj0, BigDecimal.class);
BigDecimal bd1 = (BigDecimal) coerceToNumber(ctx, obj1, BigDecimal.class);
return bd0.equals(bd1);
} else if (isDoubleOp(obj0, obj1)) {
Double d0 = (Double) coerceToNumber(ctx, obj0, Double.class);
Double d1 = (Double) coerceToNumber(ctx, obj1, Double.class);
return d0.equals(d1);
} else if (isBigIntegerOp(obj0, obj1)) {
BigInteger bi0 = (BigInteger) coerceToNumber(ctx, obj0, BigInteger.class);
BigInteger bi1 = (BigInteger) coerceToNumber(ctx, obj1, BigInteger.class);
return bi0.equals(bi1);
} else if (isLongOp(obj0, obj1)) {
Long l0 = (Long) coerceToNumber(ctx, obj0, Long.class);
Long l1 = (Long) coerceToNumber(ctx, obj1, Long.class);
return l0.equals(l1);
} else if (obj0 instanceof Boolean || obj1 instanceof Boolean) {
return coerceToBoolean(ctx, obj0, false).equals(coerceToBoolean(ctx, obj1, false));
} else if (obj0.getClass().isEnum()) {
return obj0.equals(coerceToEnum(ctx, obj1, obj0.getClass()));
} else if (obj1.getClass().isEnum()) {
return obj1.equals(coerceToEnum(ctx, obj0, obj1.getClass()));
} else if (isDateOp(obj0, obj1)) {
Instant i0 = coerceToInstant(ctx, obj0);
Instant i1 = coerceToInstant(ctx, obj1);
return i0.equals(i1);
} else if (obj0 instanceof String || obj1 instanceof String) {
int lexCompare = coerceToString(ctx, obj0).compareTo(coerceToString(ctx, obj1));
return lexCompare == 0;
} else {
return obj0.equals(obj1);
}
}
/*
* Going to have some casts /raw types somewhere so doing it here keeps them all in one place. There might be a
* neater / better solution, but I couldn't find it.
*/
@SuppressWarnings("unchecked")
public static Enum<?> coerceToEnum(final ELContext ctx, final Object obj,
@SuppressWarnings("rawtypes") Class type) {
if (ctx != null) {
boolean originalIsPropertyResolved = ctx.isPropertyResolved();
try {
Object result = ctx.getELResolver().convertToType(ctx, obj, type);
if (ctx.isPropertyResolved()) {
return (Enum<?>) result;
}
} finally {
ctx.setPropertyResolved(originalIsPropertyResolved);
}
}
if (obj == null || "".equals(obj)) {
return null;
}
if (type.isAssignableFrom(obj.getClass())) {
return (Enum<?>) obj;
}
if (!(obj instanceof String)) {
throw new ELException(MessageFactory.get("error.convert", obj, obj.getClass(), type));
}
Enum<?> result;
try {
result = Enum.valueOf(type, (String) obj);
} catch (IllegalArgumentException iae) {
throw new ELException(MessageFactory.get("error.convert", obj, obj.getClass(), type));
}
return result;
}
/**
* Convert an object to Boolean. Null and empty string are false.
*
* @param ctx the context in which this conversion is taking place
* @param obj the object to convert
* @param primitive is the target a primitive in which case coercion to null is not permitted
*
* @return the Boolean value of the object
*
* @throws ELException if object is not Boolean or String
*/
public static Boolean coerceToBoolean(final ELContext ctx, final Object obj, boolean primitive) throws ELException {
if (ctx != null) {
boolean originalIsPropertyResolved = ctx.isPropertyResolved();
try {
Boolean result = ctx.getELResolver().convertToType(ctx, obj, Boolean.class);
if (ctx.isPropertyResolved()) {
return result;
}
} finally {
ctx.setPropertyResolved(originalIsPropertyResolved);
}
}
if (!COERCE_TO_ZERO && !primitive) {
if (obj == null) {
return null;
}
}
if (obj == null || "".equals(obj)) {
return Boolean.FALSE;
}
if (obj instanceof Boolean) {
return (Boolean) obj;
}
if (obj instanceof String) {
return Boolean.valueOf((String) obj);
}
throw new ELException(MessageFactory.get("error.convert", obj, obj.getClass(), Boolean.class));
}
private static Character coerceToCharacter(final ELContext ctx, final Object obj) throws ELException {
if (ctx != null) {
boolean originalIsPropertyResolved = ctx.isPropertyResolved();
try {
Character result = ctx.getELResolver().convertToType(ctx, obj, Character.class);
if (ctx.isPropertyResolved()) {
return result;
}
} finally {
ctx.setPropertyResolved(originalIsPropertyResolved);
}
}
if (obj == null || "".equals(obj)) {
return Character.valueOf((char) 0);
}
if (obj instanceof String) {
return Character.valueOf(((String) obj).charAt(0));
}
if (ELArithmetic.isNumber(obj)) {
return Character.valueOf((char) ((Number) obj).shortValue());
}
Class<?> objType = obj.getClass();
if (obj instanceof Character) {
return (Character) obj;
}
throw new ELException(MessageFactory.get("error.convert", obj, objType, Character.class));
}
protected static Number coerceToNumber(final Number number, final Class<?> type) throws ELException {
if (Long.TYPE == type || Long.class.equals(type)) {
return Long.valueOf(number.longValue());
}
if (Double.TYPE == type || Double.class.equals(type)) {
return Double.valueOf(number.doubleValue());
}
if (Integer.TYPE == type || Integer.class.equals(type)) {
return Integer.valueOf(number.intValue());
}
if (BigInteger.class.equals(type)) {
if (number instanceof BigDecimal) {
return ((BigDecimal) number).toBigInteger();
}
if (number instanceof BigInteger) {
return number;
}
return BigInteger.valueOf(number.longValue());
}
if (BigDecimal.class.equals(type)) {
if (number instanceof BigDecimal) {
return number;
}
if (number instanceof BigInteger) {
return new BigDecimal((BigInteger) number);
}
return new BigDecimal(number.doubleValue());
}
if (Byte.TYPE == type || Byte.class.equals(type)) {
return Byte.valueOf(number.byteValue());
}
if (Short.TYPE == type || Short.class.equals(type)) {
return Short.valueOf(number.shortValue());
}
if (Float.TYPE == type || Float.class.equals(type)) {
return Float.valueOf(number.floatValue());
}
if (Number.class.equals(type)) {
return number;
}
throw new ELException(MessageFactory.get("error.convert", number, number.getClass(), type));
}
public static Number coerceToNumber(final ELContext ctx, final Object obj, final Class<?> type) throws ELException {
if (ctx != null) {
boolean originalIsPropertyResolved = ctx.isPropertyResolved();
try {
Object result = ctx.getELResolver().convertToType(ctx, obj, type);
if (ctx.isPropertyResolved()) {
return (Number) result;
}
} finally {
ctx.setPropertyResolved(originalIsPropertyResolved);
}
}
if (!COERCE_TO_ZERO) {
if (obj == null && !type.isPrimitive()) {
return null;
}
}
if (obj == null || "".equals(obj)) {
return coerceToNumber(ZERO, type);
}
if (obj instanceof String) {
return coerceToNumber((String) obj, type);
}
if (ELArithmetic.isNumber(obj)) {
return coerceToNumber((Number) obj, type);
}
if (obj instanceof Character) {
return coerceToNumber(Short.valueOf((short) ((Character) obj).charValue()), type);
}
throw new ELException(MessageFactory.get("error.convert", obj, obj.getClass(), type));
}
protected static Number coerceToNumber(final String val, final Class<?> type) throws ELException {
if (Long.TYPE == type || Long.class.equals(type)) {
try {
return Long.valueOf(val);
} catch (NumberFormatException nfe) {
throw new ELException(MessageFactory.get("error.convert", val, String.class, type));
}
}
if (Integer.TYPE == type || Integer.class.equals(type)) {
try {
return Integer.valueOf(val);
} catch (NumberFormatException nfe) {
throw new ELException(MessageFactory.get("error.convert", val, String.class, type));
}
}
if (Double.TYPE == type || Double.class.equals(type)) {
try {
return Double.valueOf(val);
} catch (NumberFormatException nfe) {
throw new ELException(MessageFactory.get("error.convert", val, String.class, type));
}
}
if (BigInteger.class.equals(type)) {
try {
return new BigInteger(val);
} catch (NumberFormatException nfe) {
throw new ELException(MessageFactory.get("error.convert", val, String.class, type));
}
}
if (BigDecimal.class.equals(type)) {
try {
return new BigDecimal(val);
} catch (NumberFormatException nfe) {
throw new ELException(MessageFactory.get("error.convert", val, String.class, type));
}
}
if (Byte.TYPE == type || Byte.class.equals(type)) {
try {
return Byte.valueOf(val);
} catch (NumberFormatException nfe) {
throw new ELException(MessageFactory.get("error.convert", val, String.class, type));
}
}
if (Short.TYPE == type || Short.class.equals(type)) {
try {
return Short.valueOf(val);
} catch (NumberFormatException nfe) {
throw new ELException(MessageFactory.get("error.convert", val, String.class, type));
}
}
if (Float.TYPE == type || Float.class.equals(type)) {
try {
return Float.valueOf(val);
} catch (NumberFormatException nfe) {
throw new ELException(MessageFactory.get("error.convert", val, String.class, type));
}
}
throw new ELException(MessageFactory.get("error.convert", val, String.class, type));
}
/**
* Coerce an object to a string.
*
* @param ctx the context in which this conversion is taking place
* @param obj the object to convert
*
* @return the String value of the object
*/
public static String coerceToString(final ELContext ctx, final Object obj) {
if (ctx != null) {
boolean originalIsPropertyResolved = ctx.isPropertyResolved();
try {
String result = ctx.getELResolver().convertToType(ctx, obj, String.class);
if (ctx.isPropertyResolved()) {
return result;
}
} finally {
ctx.setPropertyResolved(originalIsPropertyResolved);
}
}
return switch (obj) {
case null -> "";
case String s -> s;
case Enum<?> anEnum -> anEnum.name();
default -> {
try {
yield obj.toString();
} catch (ELException e) {
// Unlikely but you never know
throw e;
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
throw new ELException(t);
}
}
};
}
private static Instant coerceToInstant(final ELContext ctx, final Object obj) {
if (ctx != null) {
boolean originalIsPropertyResolved = ctx.isPropertyResolved();
try {
Instant result = ctx.getELResolver().convertToType(ctx, obj, Instant.class);
if (ctx.isPropertyResolved()) {
return result;
}
} finally {
ctx.setPropertyResolved(originalIsPropertyResolved);
}
}
return switch (obj) {
case null -> null;
case TemporalAccessor t -> Instant.from(t);
case Clock c -> c.instant();
case Date d -> d.toInstant();
case String s -> Instant.parse(s);
default -> {
throw new ELException(
MessageFactory.get("error.convert", obj, obj.getClass().getName(), Instant.class));
}
};
}
public static <T> T coerceToType(final ELContext ctx, final Object obj, final Class<T> type) throws ELException {
if (ctx != null) {
boolean originalIsPropertyResolved = ctx.isPropertyResolved();
try {
T result = ctx.getELResolver().convertToType(ctx, obj, type);
if (ctx.isPropertyResolved()) {
return result;
}
} finally {
ctx.setPropertyResolved(originalIsPropertyResolved);
}
}
if (type == null || Object.class.equals(type) || (obj != null && type.isAssignableFrom(obj.getClass()))) {
@SuppressWarnings("unchecked")
T result = (T) obj;
return result;
}
if (!COERCE_TO_ZERO) {
if (obj == null && !type.isPrimitive() && !String.class.isAssignableFrom(type)) {
return null;
}
}
if (String.class.equals(type)) {
@SuppressWarnings("unchecked")
T result = (T) coerceToString(ctx, obj);
return result;
}
if (ELArithmetic.isNumberType(type)) {
@SuppressWarnings("unchecked")
T result = (T) coerceToNumber(ctx, obj, type);
return result;
}
if (Character.class.equals(type) || Character.TYPE == type) {
@SuppressWarnings("unchecked")
T result = (T) coerceToCharacter(ctx, obj);
return result;
}
if (Boolean.class.equals(type) || Boolean.TYPE == type) {
@SuppressWarnings("unchecked")
T result = (T) coerceToBoolean(ctx, obj, Boolean.TYPE == type);
return result;
}
if (type.isEnum()) {
@SuppressWarnings("unchecked")
T result = (T) coerceToEnum(ctx, obj, type);
return result;
}
// new to spec
if (obj == null) {
return null;
}
if (Instant.class.equals(type)) {
@SuppressWarnings("unchecked")
T result = (T) coerceToInstant(ctx, obj);
return result;
}
if (Date.class.equals(type)) {
@SuppressWarnings("unchecked")
T result = (T) Date.from(coerceToInstant(ctx, obj));
return result;
}
if (obj instanceof String str) {
PropertyEditor editor = PropertyEditorManager.findEditor(type);
if (editor == null) {
if (str.isEmpty()) {
return null;
}
throw new ELException(MessageFactory.get("error.convert", obj, obj.getClass(), type));
} else {
try {
editor.setAsText(str);
@SuppressWarnings("unchecked")
T result = (T) editor.getValue();
return result;
} catch (RuntimeException e) {
if (str.isEmpty()) {
return null;
}
throw new ELException(MessageFactory.get("error.convert", obj, obj.getClass(), type), e);
}
}
}
// Handle special case because the syntax for the empty set is the same
// for an empty map. The parser will always parse {} as an empty set.
if (obj instanceof Set && type == Map.class && ((Set<?>) obj).isEmpty()) {
@SuppressWarnings("unchecked")
T result = (T) Collections.EMPTY_MAP;
return result;
}
// Handle arrays
if (type.isArray() && obj.getClass().isArray()) {
@SuppressWarnings("unchecked")
T result = (T) coerceToArray(ctx, obj, type);
return result;
}
if (obj instanceof LambdaExpression && isFunctionalInterface(type)) {
return coerceToFunctionalInterface(ctx, (LambdaExpression) obj, type);
}
throw new ELException(MessageFactory.get("error.convert", obj, obj.getClass(), type));
}
private static Object coerceToArray(final ELContext ctx, final Object obj, final Class<?> type) {
// Note: Nested arrays will result in nested calls to this method.
// Note: Calling method has checked the obj is an array.
int size = Array.getLength(obj);
// Cast the input object to an array (calling method has checked it is
// an array)
// Get the target type for the array elements
Class<?> componentType = type.getComponentType();
// Create a new array of the correct type
Object result = Array.newInstance(componentType, size);
// Coerce each element in turn.
for (int i = 0; i < size; i++) {
Array.set(result, i, coerceToType(ctx, Array.get(obj, i), componentType));
}
return result;
}
private static <T> T coerceToFunctionalInterface(final ELContext ctx, final LambdaExpression lambdaExpression,
final Class<T> type) {
Supplier<T> proxy = () -> {
// Create a dynamic proxy for the functional interface
@SuppressWarnings("unchecked")
T result = (T) Proxy.newProxyInstance(type.getClassLoader(), new Class[] { type },
(Object obj, Method method, Object[] args) -> {
// Functional interfaces have a single, abstract method
if (!Modifier.isAbstract(method.getModifiers())) {
throw new ELException(MessageFactory.get("elSupport.coerce.nonAbstract", type, method));
}
if (ctx == null) {
return lambdaExpression.invoke(args);
} else {
return lambdaExpression.invoke(ctx, args);
}
});
return result;
};
return proxy.get();
}
public static boolean isBigDecimalOp(final Object obj0, final Object obj1) {
return obj0 instanceof BigDecimal || obj1 instanceof BigDecimal;
}
public static boolean isBigIntegerOp(final Object obj0, final Object obj1) {
return obj0 instanceof BigInteger || obj1 instanceof BigInteger;
}
public static boolean isDoubleOp(final Object obj0, final Object obj1) {
return obj0 instanceof Double || obj1 instanceof Double || obj0 instanceof Float || obj1 instanceof Float;
}
public static boolean isLongOp(final Object obj0, final Object obj1) {
return obj0 instanceof Long || obj1 instanceof Long || obj0 instanceof Integer || obj1 instanceof Integer ||
obj0 instanceof Character || obj1 instanceof Character || obj0 instanceof Short ||
obj1 instanceof Short || obj0 instanceof Byte || obj1 instanceof Byte;
}
public static boolean isDateOp(final Object obj0, Object obj1) {
return obj0 instanceof TemporalAccessor || obj1 instanceof TemporalAccessor || obj0 instanceof Clock ||
obj1 instanceof Clock || obj0 instanceof Date || obj1 instanceof Date || obj0 instanceof Timestamp ||
obj1 instanceof Timestamp;
}
public static boolean isStringFloat(final String str) {
int len = str.length();
if (len > 1) {
for (int i = 0; i < len; i++) {
switch (str.charAt(i)) {
case 'E':
return true;
case 'e':
return true;
case '.':
return true;
}
}
}
return false;
}
/*
* Copied to jakarta.el.ELContext - keep in sync
*/
static boolean isFunctionalInterface(Class<?> type) {
if (!type.isInterface()) {
return false;
}
boolean foundAbstractMethod = false;
Method[] methods = type.getMethods();
for (Method method : methods) {
if (Modifier.isAbstract(method.getModifiers())) {
// Abstract methods that override one of the public methods
// of Object don't count
if (overridesObjectMethod(method)) {
continue;
}
if (foundAbstractMethod) {
// Found more than one
return false;
} else {
foundAbstractMethod = true;
}
}
}
return foundAbstractMethod;
}
/*
* Copied to jakarta.el.ELContext - keep in sync
*/
private static boolean overridesObjectMethod(Method method) {
// There are three methods that can be overridden
switch (method.getName()) {
case "equals" -> {
if (method.getReturnType().equals(boolean.class)) {
if (method.getParameterCount() == 1) {
return method.getParameterTypes()[0].equals(Object.class);
}
}
}
case "hashCode" -> {
if (method.getReturnType().equals(int.class)) {
return method.getParameterCount() == 0;
}
}
case "toString" -> {
if (method.getReturnType().equals(String.class)) {
return method.getParameterCount() == 0;
}
}
}
return false;
}
private ELSupport() {
// Utility class - hide default constructor;
}
}