All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.el.lang.ELSupport Maven / Gradle / Ivy

There is a newer version: 11.0.1
Show newest version
/*
 * 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.util.Collections;
import java.util.Map;
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
 *
 * @author Jacob Hookom [[email protected]]
 */
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.
     * 

* If the objects are identical, or they are equal according to {@link #equals(ELContext, Object, Object)} then * return 0. *

* If either object is a BigDecimal, then coerce both to BigDecimal first. Similarly for Double(Float), BigInteger, * and Long(Integer, Char, Short, Byte). *

* Otherwise, check that the first object is an instance of Comparable, and compare against the second object. If * that is null, return 1, otherwise return the result of comparing against the second object. *

* Similarly, if the second object is Comparable, if the first is null, return -1, else return the result of * comparing against the first object. *

* A null object is considered as: *

    *
  • ZERO when compared with Numbers
  • *
  • the empty string for String compares
  • *
  • Otherwise null is considered to be lower than anything else.
  • *
* * @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 final int compare(final ELContext ctx, final Object obj0, final Object obj1) throws ELException { if (obj0 == obj1 || equals(ctx, obj0, obj1)) { return 0; } 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 (obj0 instanceof String || obj1 instanceof String) { return coerceToString(ctx, obj0).compareTo(coerceToString(ctx, obj1)); } if (obj0 instanceof Comparable) { @SuppressWarnings("unchecked") // checked above final Comparable comparable = (Comparable) obj0; return (obj1 != null) ? comparable.compareTo(obj1) : 1; } if (obj1 instanceof Comparable) { @SuppressWarnings("unchecked") // checked above final Comparable comparable = (Comparable) obj1; return (obj0 != null) ? -comparable.compareTo(obj0) : -1; } throw new ELException(MessageFactory.get("error.compare", obj0, obj1)); } /** * Compare two objects for equality, after coercing to the same type if appropriate. *

* If the objects are identical (including both null) return true. *

* If either object is null, return false. *

* If either object is Boolean, coerce both to Boolean and check equality. *

* Similarly for Enum, String, BigDecimal, Double(Float), Long(Integer, Short, Byte, Character) *

* 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 final 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 (obj0 instanceof String || obj1 instanceof String) { int lexCompare = coerceToString(ctx, obj0).compareTo(coerceToString(ctx, obj1)); return (lexCompare == 0) ? true : false; } else { return obj0.equals(obj1); } } /* * Going to have 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 final 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 final Boolean coerceToBoolean(final ELContext ctx, final Object obj, boolean primitive) throws ELException { if (ctx != null) { boolean originalIsPropertyResolved = ctx.isPropertyResolved(); try { Object result = ctx.getELResolver().convertToType(ctx, obj, Boolean.class); if (ctx.isPropertyResolved()) { return (Boolean) 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 { Object result = ctx.getELResolver().convertToType(ctx, obj, Character.class); if (ctx.isPropertyResolved()) { return (Character) 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 final 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 final 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 final 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 final String coerceToString(final ELContext ctx, final Object obj) { if (ctx != null) { boolean originalIsPropertyResolved = ctx.isPropertyResolved(); try { Object result = ctx.getELResolver().convertToType(ctx, obj, String.class); if (ctx.isPropertyResolved()) { return (String) result; } } finally { ctx.setPropertyResolved(originalIsPropertyResolved); } } if (obj == null) { return ""; } else if (obj instanceof String) { return (String) obj; } else if (obj instanceof Enum) { return ((Enum) obj).name(); } else { try { return obj.toString(); } catch (ELException e) { // Unlikely but you never know throw e; } catch (Throwable t) { ExceptionUtils.handleThrowable(t); throw new ELException(t); } } } public static final T coerceToType(final ELContext ctx, final Object obj, final Class 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 (obj instanceof String) { String str = (String) obj; 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)) { T result = coerceToFunctionalInterface(ctx, (LambdaExpression) obj, type); return result; } 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 coerceToFunctionalInterface(final ELContext ctx, final LambdaExpression lambdaExpression, final Class type) { Supplier 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 final boolean isBigDecimalOp(final Object obj0, final Object obj1) { return (obj0 instanceof BigDecimal || obj1 instanceof BigDecimal); } public static final boolean isBigIntegerOp(final Object obj0, final Object obj1) { return (obj0 instanceof BigInteger || obj1 instanceof BigInteger); } public static final boolean isDoubleOp(final Object obj0, final Object obj1) { return (obj0 instanceof Double || obj1 instanceof Double || obj0 instanceof Float || obj1 instanceof Float); } public static final 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 final 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 if ("equals".equals(method.getName())) { if (method.getReturnType().equals(boolean.class)) { if (method.getParameterCount() == 1) { if (method.getParameterTypes()[0].equals(Object.class)) { return true; } } } } else if ("hashCode".equals(method.getName())) { if (method.getReturnType().equals(int.class)) { if (method.getParameterCount() == 0) { return true; } } } else if ("toString".equals(method.getName())) { if (method.getReturnType().equals(String.class)) { if (method.getParameterCount() == 0) { return true; } } } return false; } private ELSupport() { // Uility class - hide default constructor; } }