From 8579a10f598b261e04ce88f6778ead4a95c9b35a Mon Sep 17 00:00:00 2001 From: Kusal Kithul-Godage Date: Fri, 28 Feb 2025 04:07:25 +1100 Subject: [PATCH 1/2] WW-5534 Simplify ProxyUtil, add OgnlCache#computeIfAbsent --- core/pom.xml | 8 +- .../interceptor/ChainingInterceptor.java | 24 +-- .../org/apache/struts2/ognl/OgnlCache.java | 34 +++- .../struts2/ognl/OgnlCaffeineCache.java | 7 + .../apache/struts2/ognl/OgnlDefaultCache.java | 16 +- .../org/apache/struts2/ognl/OgnlLRUCache.java | 6 + .../org/apache/struts2/ognl/OgnlUtil.java | 31 +-- .../org/apache/struts2/util/ProxyUtil.java | 180 ++++++------------ .../struts2/json/DefaultJSONWriter.java | 6 +- 9 files changed, 149 insertions(+), 163 deletions(-) diff --git a/core/pom.xml b/core/pom.xml index 2e10aafccd..a70b30133c 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -187,13 +187,19 @@ commons-text - + org.hibernate hibernate-core 5.6.15.Final true + + + org.springframework + spring-aop + true + org.springframework diff --git a/core/src/main/java/org/apache/struts2/interceptor/ChainingInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/ChainingInterceptor.java index c114d65e1e..2837d4c105 100644 --- a/core/src/main/java/org/apache/struts2/interceptor/ChainingInterceptor.java +++ b/core/src/main/java/org/apache/struts2/interceptor/ChainingInterceptor.java @@ -20,11 +20,11 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.apache.struts2.result.ActionChainResult; import org.apache.struts2.ActionInvocation; import org.apache.struts2.StrutsConstants; import org.apache.struts2.Unchainable; import org.apache.struts2.inject.Inject; +import org.apache.struts2.result.ActionChainResult; import org.apache.struts2.result.Result; import org.apache.struts2.util.CompoundRoot; import org.apache.struts2.util.ProxyUtil; @@ -167,17 +167,18 @@ public String intercept(ActionInvocation invocation) throws Exception { } private void copyStack(ActionInvocation invocation, CompoundRoot root) { - List list = prepareList(root); + List list = prepareList(root); Map ctxMap = invocation.getInvocationContext().getContextMap(); for (Object object : list) { - if (shouldCopy(object)) { - Object action = invocation.getAction(); - Class editable = null; - if(ProxyUtil.isProxy(action)) { - editable = ProxyUtil.ultimateTargetClass(action); - } - reflectionProvider.copy(object, action, ctxMap, prepareExcludes(), includes, editable); + if (!shouldCopy(object)) { + continue; + } + Object action = invocation.getAction(); + Class editable = null; + if (ProxyUtil.isProxy(action)) { + editable = ProxyUtil.ultimateTargetClass(action); } + reflectionProvider.copy(object, action, ctxMap, prepareExcludes(), includes, editable); } } @@ -204,9 +205,8 @@ private boolean shouldCopy(Object o) { return o != null && !(o instanceof Unchainable); } - @SuppressWarnings("unchecked") - private List prepareList(CompoundRoot root) { - List list = new ArrayList(root); + private List prepareList(CompoundRoot root) { + var list = new ArrayList<>(root); list.remove(0); Collections.reverse(list); return list; diff --git a/core/src/main/java/org/apache/struts2/ognl/OgnlCache.java b/core/src/main/java/org/apache/struts2/ognl/OgnlCache.java index ec2d002e97..d7c88b3368 100644 --- a/core/src/main/java/org/apache/struts2/ognl/OgnlCache.java +++ b/core/src/main/java/org/apache/struts2/ognl/OgnlCache.java @@ -15,21 +15,37 @@ */ package org.apache.struts2.ognl; +import java.util.function.Function; + +import static java.util.Objects.requireNonNull; + /** - * A basic cache interface for use with OGNL processing (such as Expression, BeanInfo). - * All OGNL caches will have an eviction limit, but setting an extremely high value can - * simulate an "effectively unlimited" cache. + * A basic cache interface for use with OGNL processing (such as Expression, BeanInfo). Implementation must be + * thread-safe. All OGNL caches will have an eviction limit, but setting an extremely high value can simulate an + * "effectively unlimited" cache. * - * @param The type for the cache key entries - * @param The type for the cache value entries + * @param The type for the cache key entries + * @param The type for the cache value entries */ -public interface OgnlCache { +public interface OgnlCache { + + V get(K key); - Value get(Key key); + /** + * @since 7.1 + */ + default V computeIfAbsent(K key, + Function mappingFunction) { + requireNonNull(mappingFunction); + if (get(key) == null) { + putIfAbsent(key, mappingFunction.apply(key)); + } + return get(key); + } - void put(Key key, Value value); + void put(K key, V value); - void putIfAbsent(Key key, Value value); + void putIfAbsent(K key, V value); int size(); diff --git a/core/src/main/java/org/apache/struts2/ognl/OgnlCaffeineCache.java b/core/src/main/java/org/apache/struts2/ognl/OgnlCaffeineCache.java index 22ba4c62ec..b7f5b2750b 100644 --- a/core/src/main/java/org/apache/struts2/ognl/OgnlCaffeineCache.java +++ b/core/src/main/java/org/apache/struts2/ognl/OgnlCaffeineCache.java @@ -18,6 +18,8 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; +import java.util.function.Function; + /** *

This OGNL Cache implementation is backed by {@link Caffeine} which uses the Window TinyLfu algorithm.

* @@ -46,6 +48,11 @@ public V get(K key) { return cache.getIfPresent(key); } + @Override + public V computeIfAbsent(K key, Function mappingFunction) { + return cache.asMap().computeIfAbsent(key, mappingFunction); + } + @Override public void put(K key, V value) { cache.put(key, value); diff --git a/core/src/main/java/org/apache/struts2/ognl/OgnlDefaultCache.java b/core/src/main/java/org/apache/struts2/ognl/OgnlDefaultCache.java index 727c2177d1..391944a10c 100644 --- a/core/src/main/java/org/apache/struts2/ognl/OgnlDefaultCache.java +++ b/core/src/main/java/org/apache/struts2/ognl/OgnlDefaultCache.java @@ -17,6 +17,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; /** *

Basic OGNL cache implementation.

@@ -45,16 +46,21 @@ public V get(K key) { return ognlCache.get(key); } + @Override + public V computeIfAbsent(K key, Function mappingFunction) { + return ognlCache.computeIfAbsent(key, mappingFunction); + } + @Override public void put(K key, V value) { ognlCache.put(key, value); - this.clearIfEvictionLimitExceeded(); + clearIfEvictionLimitExceeded(); } @Override public void putIfAbsent(K key, V value) { ognlCache.putIfAbsent(key, value); - this.clearIfEvictionLimitExceeded(); + clearIfEvictionLimitExceeded(); } @Override @@ -69,12 +75,12 @@ public void clear() { @Override public int getEvictionLimit() { - return this.cacheEvictionLimit.get(); + return cacheEvictionLimit.get(); } @Override - public void setEvictionLimit(int cacheEvictionLimit) { - this.cacheEvictionLimit.set(cacheEvictionLimit); + public void setEvictionLimit(int newCacheEvictionLimit) { + cacheEvictionLimit.set(newCacheEvictionLimit); } /** diff --git a/core/src/main/java/org/apache/struts2/ognl/OgnlLRUCache.java b/core/src/main/java/org/apache/struts2/ognl/OgnlLRUCache.java index 6b43d71f23..fc995d438c 100644 --- a/core/src/main/java/org/apache/struts2/ognl/OgnlLRUCache.java +++ b/core/src/main/java/org/apache/struts2/ognl/OgnlLRUCache.java @@ -19,6 +19,7 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; /** *

A basic OGNL LRU cache implementation.

@@ -54,6 +55,11 @@ public V get(K key) { return ognlLRUCache.get(key); } + @Override + public V computeIfAbsent(K key, Function mappingFunction) { + return ognlLRUCache.computeIfAbsent(key, mappingFunction); + } + @Override public void put(K key, V value) { ognlLRUCache.put(key, value); diff --git a/core/src/main/java/org/apache/struts2/ognl/OgnlUtil.java b/core/src/main/java/org/apache/struts2/ognl/OgnlUtil.java index 95bee5f430..4d51ff2949 100644 --- a/core/src/main/java/org/apache/struts2/ognl/OgnlUtil.java +++ b/core/src/main/java/org/apache/struts2/ognl/OgnlUtil.java @@ -18,12 +18,6 @@ */ package org.apache.struts2.ognl; -import org.apache.struts2.conversion.impl.XWorkConverter; -import org.apache.struts2.inject.Container; -import org.apache.struts2.inject.Inject; -import org.apache.struts2.ognl.accessor.RootAccessor; -import org.apache.struts2.util.CompoundRoot; -import org.apache.struts2.util.reflection.ReflectionException; import ognl.ClassResolver; import ognl.Ognl; import ognl.OgnlContext; @@ -35,7 +29,12 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.struts2.StrutsConstants; -import org.apache.struts2.ognl.OgnlGuard; +import org.apache.struts2.conversion.impl.XWorkConverter; +import org.apache.struts2.inject.Container; +import org.apache.struts2.inject.Inject; +import org.apache.struts2.ognl.accessor.RootAccessor; +import org.apache.struts2.util.CompoundRoot; +import org.apache.struts2.util.reflection.ReflectionException; import java.beans.BeanInfo; import java.beans.IntrospectionException; @@ -676,13 +675,19 @@ public BeanInfo getBeanInfo(Object from) throws IntrospectionException { * @throws IntrospectionException is thrown if an exception occurs during introspection. */ public BeanInfo getBeanInfo(Class clazz) throws IntrospectionException { - synchronized (beanInfoCache) { - BeanInfo beanInfo = beanInfoCache.get(clazz); - if (beanInfo == null) { - beanInfo = Introspector.getBeanInfo(clazz, Object.class); - beanInfoCache.putIfAbsent(clazz, beanInfo); + try { + return beanInfoCache.computeIfAbsent(clazz, k -> { + try { + return Introspector.getBeanInfo(k, Object.class); + } catch (IntrospectionException e) { + throw new IllegalArgumentException(e); + } + }); + } catch (IllegalArgumentException e) { + if (e.getCause() instanceof IntrospectionException innerEx) { + throw innerEx; } - return beanInfo; + throw e; } } diff --git a/core/src/main/java/org/apache/struts2/util/ProxyUtil.java b/core/src/main/java/org/apache/struts2/util/ProxyUtil.java index 71823b47e5..a574af1c22 100644 --- a/core/src/main/java/org/apache/struts2/util/ProxyUtil.java +++ b/core/src/main/java/org/apache/struts2/util/ProxyUtil.java @@ -26,15 +26,19 @@ import org.apache.struts2.ognl.OgnlCacheFactory; import org.hibernate.Hibernate; import org.hibernate.proxy.HibernateProxy; +import org.springframework.aop.SpringProxy; +import org.springframework.aop.TargetClassAware; +import org.springframework.aop.framework.Advised; +import org.springframework.aop.framework.AopProxyUtils; +import org.springframework.aop.support.AopUtils; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Member; import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.lang.reflect.Proxy; import static java.lang.reflect.Modifier.isPublic; +import static java.lang.reflect.Modifier.isStatic; /** * ProxyUtil @@ -44,17 +48,14 @@ * */ public class ProxyUtil { - private static final String SPRING_ADVISED_CLASS_NAME = "org.springframework.aop.framework.Advised"; - private static final String SPRING_SPRINGPROXY_CLASS_NAME = "org.springframework.aop.SpringProxy"; - private static final String SPRING_SINGLETONTARGETSOURCE_CLASS_NAME = "org.springframework.aop.target.SingletonTargetSource"; - private static final String SPRING_TARGETCLASSAWARE_CLASS_NAME = "org.springframework.aop.TargetClassAware"; - private static final String HIBERNATE_HIBERNATEPROXY_CLASS_NAME = "org.hibernate.proxy.HibernateProxy"; private static final int CACHE_MAX_SIZE = 10000; private static final int CACHE_INITIAL_CAPACITY = 256; private static final OgnlCache, Boolean> isProxyCache = new DefaultOgnlCacheFactory, Boolean>( CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.WTLFU, CACHE_INITIAL_CAPACITY).buildOgnlCache(); private static final OgnlCache isProxyMemberCache = new DefaultOgnlCacheFactory( CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.WTLFU, CACHE_INITIAL_CAPACITY).buildOgnlCache(); + private static final OgnlCache> targetClassCache = new DefaultOgnlCacheFactory>( + CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.WTLFU, CACHE_INITIAL_CAPACITY).buildOgnlCache(); /** * Determine the ultimate target class of the given instance, traversing @@ -65,15 +66,18 @@ public class ProxyUtil { * object as fallback; never {@code null}) */ public static Class ultimateTargetClass(Object candidate) { - Class result = null; - if (isSpringAopProxy(candidate)) - result = springUltimateTargetClass(candidate); - - if (result == null) { - result = candidate.getClass(); - } - - return result; + return targetClassCache.computeIfAbsent(candidate, k -> { + Class result = null; + if (isSpringAopProxy(k)) { + result = springUltimateTargetClass(k); + } else if (isHibernateProxy(k)) { + result = getHibernateProxyTarget(k).getClass(); + } + if (result == null) { + result = k.getClass(); + } + return result; + }); } /** @@ -82,16 +86,8 @@ public static Class ultimateTargetClass(Object candidate) { */ public static boolean isProxy(Object object) { if (object == null) return false; - Class clazz = object.getClass(); - Boolean flag = isProxyCache.get(clazz); - if (flag != null) { - return flag; - } - - boolean isProxy = isSpringAopProxy(object) || isHibernateProxy(object); - - isProxyCache.put(clazz, isProxy); - return isProxy; + return isProxyCache.computeIfAbsent(object.getClass(), + k -> isSpringAopProxy(object) || isHibernateProxy(object)); } /** @@ -100,19 +96,11 @@ public static boolean isProxy(Object object) { * @param object the object to check */ public static boolean isProxyMember(Member member, Object object) { - if (!Modifier.isStatic(member.getModifiers()) && !isProxy(object) && !isHibernateProxy(object)) { + if (!isStatic(member.getModifiers()) && !isProxy(object)) { return false; } - - Boolean flag = isProxyMemberCache.get(member); - if (flag != null) { - return flag; - } - - boolean isProxyMember = isSpringProxyMember(member) || isHibernateProxyMember(member); - - isProxyMemberCache.put(member, isProxyMember); - return isProxyMember; + return isProxyMemberCache.computeIfAbsent(member, + k -> isSpringProxyMember(member) || isHibernateProxyMember(member)); } /** @@ -123,7 +111,7 @@ public static boolean isProxyMember(Member member, Object object) { public static boolean isHibernateProxy(Object object) { try { return object != null && HibernateProxy.class.isAssignableFrom(object.getClass()); - } catch (NoClassDefFoundError ignored) { + } catch (LinkageError ignored) { return false; } } @@ -135,12 +123,10 @@ public static boolean isHibernateProxy(Object object) { */ public static boolean isHibernateProxyMember(Member member) { try { - Class clazz = ClassLoaderUtil.loadClass(HIBERNATE_HIBERNATEPROXY_CLASS_NAME, ProxyUtil.class); - return hasMember(clazz, member); - } catch (ClassNotFoundException ignored) { + return hasMember(HibernateProxy.class, member); + } catch (LinkageError ignored) { + return false; } - - return false; } /** @@ -152,20 +138,11 @@ public static boolean isHibernateProxyMember(Member member) { * object as fallback; never {@code null}) */ private static Class springUltimateTargetClass(Object candidate) { - Object current = candidate; - Class result = null; - while (null != current && implementsInterface(current.getClass(), SPRING_TARGETCLASSAWARE_CLASS_NAME)) { - try { - result = (Class) MethodUtils.invokeMethod(current, "getTargetClass"); - } catch (Throwable ignored) { - } - current = getSingletonTarget(current); - } - if (result == null) { - Class clazz = candidate.getClass(); - result = (isCglibProxyClass(clazz) ? clazz.getSuperclass() : candidate.getClass()); + try { + return AopProxyUtils.ultimateTargetClass(candidate); + } catch (LinkageError ignored) { + return candidate.getClass(); } - return result; } /** @@ -173,9 +150,11 @@ private static Class springUltimateTargetClass(Object candidate) { * @param object the object to check */ private static boolean isSpringAopProxy(Object object) { - Class clazz = object.getClass(); - return (implementsInterface(clazz, SPRING_SPRINGPROXY_CLASS_NAME) && (Proxy.isProxyClass(clazz) - || isCglibProxyClass(clazz))); + try { + return AopUtils.isAopProxy(object); + } catch (LinkageError ignored) { + return false; + } } /** @@ -184,79 +163,32 @@ private static boolean isSpringAopProxy(Object object) { */ private static boolean isSpringProxyMember(Member member) { try { - Class clazz = ClassLoaderUtil.loadClass(SPRING_ADVISED_CLASS_NAME, ProxyUtil.class); - if (hasMember(clazz, member)) + if (hasMember(Advised.class, member)) return true; - clazz = ClassLoaderUtil.loadClass(SPRING_TARGETCLASSAWARE_CLASS_NAME, ProxyUtil.class); - if (hasMember(clazz, member)) + if (hasMember(TargetClassAware.class, member)) return true; - clazz = ClassLoaderUtil.loadClass(SPRING_SPRINGPROXY_CLASS_NAME, ProxyUtil.class); - if (hasMember(clazz, member)) + if (hasMember(SpringProxy.class, member)) return true; - } catch (ClassNotFoundException ignored) { + } catch (LinkageError ignored) { } - return false; } - /** - * Obtain the singleton target object behind the given spring proxy, if any. - * @param candidate the (potential) spring proxy to check - * @return the singleton target object, or {@code null} in any other case - * (not a spring proxy, not an existing singleton target) - */ - private static Object getSingletonTarget(Object candidate) { - try { - if (implementsInterface(candidate.getClass(), SPRING_ADVISED_CLASS_NAME)) { - Object targetSource = MethodUtils.invokeMethod(candidate, "getTargetSource"); - if (implementsInterface(targetSource.getClass(), SPRING_SINGLETONTARGETSOURCE_CLASS_NAME)) { - return MethodUtils.invokeMethod(targetSource, "getTarget"); - } - } - } catch (Throwable ignored) { - } - - return null; - } - - /** - * Check whether the specified class is a CGLIB-generated class. - * @param clazz the class to check - */ - private static boolean isCglibProxyClass(Class clazz) { - return (clazz != null && clazz.getName().contains("$$")); - } - - /** - * Check whether the given class implements an interface with a given class name. - * @param clazz the class to check - * @param ifaceClassName the interface class name to check - */ - private static boolean implementsInterface(Class clazz, String ifaceClassName) { - try { - Class ifaceClass = ClassLoaderUtil.loadClass(ifaceClassName, ProxyUtil.class); - return ifaceClass.isAssignableFrom(clazz); - } catch (ClassNotFoundException e) { - return false; - } - } - /** * Check whether the given class has a given member. * @param clazz the class to check * @param member the member to check */ private static boolean hasMember(Class clazz, Member member) { - if (member instanceof Method) { - return null != MethodUtils.getMatchingMethod(clazz, member.getName(), ((Method) member).getParameterTypes()); + if (member instanceof Method method) { + return null != MethodUtils.getMatchingMethod(clazz, member.getName(), method.getParameterTypes()); } if (member instanceof Field) { return null != FieldUtils.getField(clazz, member.getName(), true); } - if (member instanceof Constructor) { - return null != ConstructorUtils.getMatchingAccessibleConstructor(clazz, ((Constructor) member).getParameterTypes()); + if (member instanceof Constructor constructor) { + return null != ConstructorUtils.getMatchingAccessibleConstructor(clazz, constructor.getParameterTypes()); } - return false; } @@ -266,26 +198,34 @@ private static boolean hasMember(Class clazz, Member member) { public static Object getHibernateProxyTarget(Object object) { try { return Hibernate.unproxy(object); - } catch (NoClassDefFoundError ignored) { + } catch (LinkageError ignored) { return object; } } /** - * @return matching member on target object if one exists, otherwise the same member + * @deprecated since 7.1, use {@link #resolveTargetMember(Member, Class)} instead. */ + @Deprecated public static Member resolveTargetMember(Member proxyMember, Object target) { + return resolveTargetMember(proxyMember, target.getClass()); + } + + /** + * @return matching member on target object if one exists, otherwise the same member + */ + public static Member resolveTargetMember(Member proxyMember, Class targetClass) { int mod = proxyMember.getModifiers(); if (proxyMember instanceof Method) { if (isPublic(mod)) { - return MethodUtils.getMatchingAccessibleMethod(target.getClass(), proxyMember.getName(), ((Method) proxyMember).getParameterTypes()); + return MethodUtils.getMatchingAccessibleMethod(targetClass, proxyMember.getName(), ((Method) proxyMember).getParameterTypes()); } else { - return MethodUtils.getMatchingMethod(target.getClass(), proxyMember.getName(), ((Method) proxyMember).getParameterTypes()); + return MethodUtils.getMatchingMethod(targetClass, proxyMember.getName(), ((Method) proxyMember).getParameterTypes()); } } else if (proxyMember instanceof Field) { - return FieldUtils.getField(target.getClass(), proxyMember.getName(), isPublic(mod)); + return FieldUtils.getField(targetClass, proxyMember.getName(), isPublic(mod)); } else if (proxyMember instanceof Constructor && isPublic(mod)) { - return ConstructorUtils.getMatchingAccessibleConstructor(target.getClass(), ((Constructor) proxyMember).getParameterTypes()); + return ConstructorUtils.getMatchingAccessibleConstructor(targetClass, ((Constructor) proxyMember).getParameterTypes()); } return proxyMember; } diff --git a/plugins/json/src/main/java/org/apache/struts2/json/DefaultJSONWriter.java b/plugins/json/src/main/java/org/apache/struts2/json/DefaultJSONWriter.java index d6328af526..8e768bd915 100644 --- a/plugins/json/src/main/java/org/apache/struts2/json/DefaultJSONWriter.java +++ b/plugins/json/src/main/java/org/apache/struts2/json/DefaultJSONWriter.java @@ -18,15 +18,15 @@ */ package org.apache.struts2.json; -import org.apache.struts2.inject.Inject; -import org.apache.struts2.util.ProxyUtil; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.struts2.inject.Inject; import org.apache.struts2.json.annotations.JSON; import org.apache.struts2.json.annotations.JSONFieldBridge; import org.apache.struts2.json.annotations.JSONParameter; import org.apache.struts2.json.bridge.FieldBridge; import org.apache.struts2.json.bridge.ParameterizedBridge; +import org.apache.struts2.util.ProxyUtil; import java.beans.BeanInfo; import java.beans.IntrospectionException; @@ -221,7 +221,7 @@ protected void bean(Object object) throws JSONException { BeanInfo info; try { - Class clazz = excludeProxyProperties ? ProxyUtil.ultimateTargetClass(object) : object.getClass(); + Class clazz = excludeProxyProperties ? ProxyUtil.ultimateTargetClass(object) : object.getClass(); info = ((object == this.root) && this.ignoreHierarchy) ? getBeanInfoIgnoreHierarchy(clazz) From 117b9c741bc487c0c987240e1bafae3bd024980e Mon Sep 17 00:00:00 2001 From: Kusal Kithul-Godage Date: Mon, 3 Mar 2025 20:56:18 +1100 Subject: [PATCH 2/2] WW-5534 Add coverage for OgnlUtil#getBeanInfo exception propagation --- .../org/apache/struts2/ognl/OgnlUtilTest.java | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/core/src/test/java/org/apache/struts2/ognl/OgnlUtilTest.java b/core/src/test/java/org/apache/struts2/ognl/OgnlUtilTest.java index 9c526ffcee..a160f64180 100644 --- a/core/src/test/java/org/apache/struts2/ognl/OgnlUtilTest.java +++ b/core/src/test/java/org/apache/struts2/ognl/OgnlUtilTest.java @@ -18,8 +18,18 @@ */ package org.apache.struts2.ognl; +import ognl.InappropriateExpressionException; +import ognl.MethodFailedException; +import ognl.NoSuchPropertyException; +import ognl.NullHandler; +import ognl.Ognl; +import ognl.OgnlContext; +import ognl.OgnlException; +import ognl.OgnlRuntime; +import ognl.SimpleNode; import org.apache.struts2.ActionContext; -import org.apache.struts2.text.StubTextProvider; +import org.apache.struts2.StrutsConstants; +import org.apache.struts2.StrutsException; import org.apache.struts2.StubValueStack; import org.apache.struts2.XWorkTestCase; import org.apache.struts2.config.ConfigurationException; @@ -30,6 +40,7 @@ import org.apache.struts2.ognl.accessor.RootAccessor; import org.apache.struts2.test.StubConfigurationProvider; import org.apache.struts2.test.User; +import org.apache.struts2.text.StubTextProvider; import org.apache.struts2.util.Bar; import org.apache.struts2.util.CompoundRoot; import org.apache.struts2.util.Foo; @@ -37,20 +48,11 @@ import org.apache.struts2.util.ValueStack; import org.apache.struts2.util.location.LocatableProperties; import org.apache.struts2.util.reflection.ReflectionContextState; -import ognl.InappropriateExpressionException; -import ognl.MethodFailedException; -import ognl.NoSuchPropertyException; -import ognl.NullHandler; -import ognl.Ognl; -import ognl.OgnlContext; -import ognl.OgnlException; -import ognl.OgnlRuntime; -import ognl.SimpleNode; -import org.apache.struts2.StrutsConstants; -import org.apache.struts2.StrutsException; +import org.mockito.MockedStatic; import java.beans.BeanInfo; import java.beans.IntrospectionException; +import java.beans.Introspector; import java.lang.reflect.Method; import java.text.DateFormat; import java.util.ArrayList; @@ -66,6 +68,7 @@ import static org.apache.struts2.ognl.OgnlCacheFactory.CacheType.LRU; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mockStatic; public class OgnlUtilTest extends XWorkTestCase { @@ -432,6 +435,17 @@ public void testBeanInfoLRUCacheLimits() throws IntrospectionException { assertNotSame("BeanInfo dropped from LRU cache is the same as newly added ?", beanInfo1_1, beanInfo1_4); } + /** + * Ensure any {@link IntrospectionException} thrown by {@link Introspector} are propagated as is. + */ + public void testBeanInfoCacheExceptionHandling() { + try (MockedStatic introspector = mockStatic(Introspector.class)) { + var exception = new IntrospectionException("Test Exception"); + introspector.when(() -> Introspector.getBeanInfo(TestBean1.class, Object.class)).thenThrow(exception); + assertSame(exception, assertThrows(IntrospectionException.class, () -> ognlUtil.getBeanInfo(new TestBean1()))); + } + } + public void testClearRuntimeCache() { // Confirm that no exceptions or failures arise when calling the convenience global clear method. OgnlUtil.clearRuntimeCache();