Compare commits

...
12 Commits
Author SHA1 Message Date
Spring Builds aca8b5d2f5 Release v5.3.37 2024-06-13 08:27:22 +00:00
Juergen Hoeller 0ce1ef97ce Polishing (aligned with 6.0.x) 2024-06-12 14:10:08 +02:00
Juergen Hoeller e26ea007fa Upgrade to spring-javaformat-checkstyle 0.0.42 and Netty 4.1.111 2024-06-12 14:09:08 +02:00
Stéphane Nicoll c10b6c2c3e Upgrade to Reactor 2020.0.45
Closes gh-33010
2024-06-11 14:02:14 +02:00
Juergen Hoeller 63568e6c4f Skip ajc-compiled aspects for ajc-compiled target classes
Includes defensive ignoring of incompatible aspect types.

Closes gh-32970

(cherry picked from commit 0ea96b4806)
2024-06-06 21:09:27 +02:00
Juergen Hoeller d41048528f Avoid NoSuchMethodException for annotation attribute checks
Closes gh-32921

(cherry picked from commit b08883b65c)
2024-06-03 12:56:36 +02:00
Sam Brannen 2384474615 Support compilation of map indexing with primitive in SpEL
Prior to this commit, the Spring Expression Language (SpEL) failed to
compile an expression that indexed into a Map using a primitive literal
(boolean, int, long, float, or double).

This commit adds support for compilation of such expressions by
ensuring that primitive literals are boxed into their corresponding
wrapper types in the compiled bytecode.

Closes gh-32903

(cherry picked from commit aed1d5f762)
2024-05-28 10:55:22 +02:00
Sam Brannen 0f04052ba1 Support compilation of array and list indexing with Integer in SpEL
Prior to this commit, the Spring Expression Language (SpEL) failed to
compile an expression that indexed into an array or list using an
Integer.

This commit adds support for compilation of such expressions by
ensuring that an Integer is unboxed into an int in the compiled
bytecode.

See gh-32694
Closes gh-32908

(cherry picked from commit 079d53c8d6)
2024-05-27 17:39:43 +02:00
Juergen Hoeller 98aa03c0c9 Test detection of original generic method for CGLIB bridge method
Includes isBridgedCandidateFor optimization (aligned with 6.0.x)

See gh-32888
2024-05-24 12:41:38 +02:00
Juergen Hoeller 3e45b76132 Polishing
(cherry picked from commit 6c08d93992)
2024-05-23 17:21:34 +02:00
Juergen Hoeller d42c9204ef Defensive handling of incompatible advice methods
This covers AspectJ transaction and caching aspects when encountered by Spring AOP.

Closes gh-32882
See gh-32793

(cherry picked from commit 6d7cd9c7dc)
2024-05-23 17:17:03 +02:00
Spring Builds d2babd46a2 Next development version (v5.3.37-SNAPSHOT) 2024-05-22 15:27:49 +00:00
17 changed files with 738 additions and 132 deletions
+3 -3
View File
@@ -28,8 +28,8 @@ configure(allprojects) { project ->
dependencyManagement {
imports {
mavenBom "com.fasterxml.jackson:jackson-bom:2.12.7"
mavenBom "io.netty:netty-bom:4.1.109.Final"
mavenBom "io.projectreactor:reactor-bom:2020.0.44"
mavenBom "io.netty:netty-bom:4.1.111.Final"
mavenBom "io.projectreactor:reactor-bom:2020.0.45"
mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR13"
mavenBom "io.rsocket:rsocket-bom:1.1.3"
mavenBom "org.eclipse.jetty:jetty-bom:9.4.54.v20240208"
@@ -362,7 +362,7 @@ configure([rootProject] + javaProjects) { project ->
// JSR-305 only used for non-required meta-annotations
compileOnly("com.google.code.findbugs:jsr305")
testCompileOnly("com.google.code.findbugs:jsr305")
checkstyle("io.spring.javaformat:spring-javaformat-checkstyle:0.0.41")
checkstyle("io.spring.javaformat:spring-javaformat-checkstyle:0.0.42")
}
ext.javadocLinks = [
+1 -1
View File
@@ -1,4 +1,4 @@
version=5.3.36-SNAPSHOT
version=5.3.37
org.gradle.jvmargs=-Xmx2048m
org.gradle.caching=true
org.gradle.parallel=true
@@ -63,7 +63,7 @@ class EnableCachingIntegrationTests {
// attempt was made to look up the AJ aspect. It's due to classpath issues
// in .integration-tests that it's not found.
assertThatException().isThrownBy(ctx::refresh)
.withMessageContaining("AspectJCachingConfiguration");
.withMessageContaining("AspectJCachingConfiguration");
}
@@ -561,8 +561,7 @@ public class EnvironmentSystemIntegrationTests {
{
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setRequiredProperties("foo", "bar");
assertThatExceptionOfType(MissingRequiredPropertiesException.class).isThrownBy(
ctx::refresh);
assertThatExceptionOfType(MissingRequiredPropertiesException.class).isThrownBy(ctx::refresh);
}
{
@@ -97,7 +97,7 @@ class EnableTransactionManagementIntegrationTests {
ctx.register(Config.class, AspectJTxConfig.class);
// this test is a bit fragile, but gets the job done, proving that an
// attempt was made to look up the AJ aspect. It's due to classpath issues
// in .integration-tests that it's not found.
// in integration-tests that it's not found.
assertThatException()
.isThrownBy(ctx::refresh)
.withMessageContaining("AspectJJtaTransactionManagementConfiguration");
@@ -18,6 +18,7 @@ package org.springframework.aop.aspectj;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
@@ -86,6 +87,8 @@ import org.springframework.util.StringUtils;
public class AspectJExpressionPointcut extends AbstractExpressionPointcut
implements ClassFilter, IntroductionAwareMethodMatcher, BeanFactoryAware {
private static final String AJC_MAGIC = "ajc$";
private static final Set<PointcutPrimitive> SUPPORTED_PRIMITIVES = new HashSet<>();
static {
@@ -107,6 +110,8 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
@Nullable
private Class<?> pointcutDeclarationScope;
private boolean aspectCompiledByAjc;
private String[] pointcutParameterNames = new String[0];
private Class<?>[] pointcutParameterTypes = new Class<?>[0];
@@ -138,7 +143,7 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
* @param paramTypes the parameter types for the pointcut
*/
public AspectJExpressionPointcut(Class<?> declarationScope, String[] paramNames, Class<?>[] paramTypes) {
this.pointcutDeclarationScope = declarationScope;
setPointcutDeclarationScope(declarationScope);
if (paramNames.length != paramTypes.length) {
throw new IllegalStateException(
"Number of pointcut parameter names must match number of pointcut parameter types");
@@ -153,6 +158,7 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
*/
public void setPointcutDeclarationScope(Class<?> pointcutDeclarationScope) {
this.pointcutDeclarationScope = pointcutDeclarationScope;
this.aspectCompiledByAjc = compiledByAjc(pointcutDeclarationScope);
}
/**
@@ -278,6 +284,11 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
@Override
public boolean matches(Class<?> targetClass) {
if (this.pointcutParsingFailed) {
// Pointcut parsing failed before below -> avoid trying again.
return false;
}
if (this.aspectCompiledByAjc && compiledByAjc(targetClass)) {
// ajc-compiled aspect class for ajc-compiled target class -> already weaved.
return false;
}
@@ -532,6 +543,15 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
return shadowMatch;
}
private static boolean compiledByAjc(Class<?> clazz) {
for (Field field : clazz.getDeclaredFields()) {
if (field.getName().startsWith(AJC_MAGIC)) {
return true;
}
}
return false;
}
@Override
public boolean equals(@Nullable Object other) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -22,9 +22,12 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.aspectj.lang.reflect.PerClauseKind;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.AopConfigException;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.lang.Nullable;
@@ -40,6 +43,8 @@ import org.springframework.util.Assert;
*/
public class BeanFactoryAspectJAdvisorsBuilder {
private static final Log logger = LogFactory.getLog(BeanFactoryAspectJAdvisorsBuilder.class);
private final ListableBeanFactory beanFactory;
private final AspectJAdvisorFactory advisorFactory;
@@ -102,30 +107,37 @@ public class BeanFactoryAspectJAdvisorsBuilder {
continue;
}
if (this.advisorFactory.isAspect(beanType)) {
aspectNames.add(beanName);
AspectMetadata amd = new AspectMetadata(beanType, beanName);
if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) {
MetadataAwareAspectInstanceFactory factory =
new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);
List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory);
if (this.beanFactory.isSingleton(beanName)) {
this.advisorsCache.put(beanName, classAdvisors);
try {
AspectMetadata amd = new AspectMetadata(beanType, beanName);
if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) {
MetadataAwareAspectInstanceFactory factory =
new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);
List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory);
if (this.beanFactory.isSingleton(beanName)) {
this.advisorsCache.put(beanName, classAdvisors);
}
else {
this.aspectFactoryCache.put(beanName, factory);
}
advisors.addAll(classAdvisors);
}
else {
// Per target or per this.
if (this.beanFactory.isSingleton(beanName)) {
throw new IllegalArgumentException("Bean with name '" + beanName +
"' is a singleton, but aspect instantiation model is not singleton");
}
MetadataAwareAspectInstanceFactory factory =
new PrototypeAspectInstanceFactory(this.beanFactory, beanName);
this.aspectFactoryCache.put(beanName, factory);
advisors.addAll(this.advisorFactory.getAdvisors(factory));
}
advisors.addAll(classAdvisors);
aspectNames.add(beanName);
}
else {
// Per target or per this.
if (this.beanFactory.isSingleton(beanName)) {
throw new IllegalArgumentException("Bean with name '" + beanName +
"' is a singleton, but aspect instantiation model is not singleton");
catch (IllegalArgumentException | IllegalStateException | AopConfigException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Ignoring incompatible aspect [" + beanType.getName() + "]: " + ex);
}
MetadataAwareAspectInstanceFactory factory =
new PrototypeAspectInstanceFactory(this.beanFactory, beanName);
this.aspectFactoryCache.put(beanName, factory);
advisors.addAll(this.advisorFactory.getAdvisors(factory));
}
}
}
@@ -213,8 +213,16 @@ public class ReflectiveAspectJAdvisorFactory extends AbstractAspectJAdvisorFacto
return null;
}
return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod,
this, aspectInstanceFactory, declarationOrderInAspect, aspectName);
try {
return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod,
this, aspectInstanceFactory, declarationOrderInAspect, aspectName);
}
catch (IllegalArgumentException | IllegalStateException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Ignoring incompatible advice method: " + candidateAdviceMethod, ex);
}
return null;
}
}
@Nullable
@@ -2,21 +2,29 @@
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:cache="http://www.springframework.org/schema/cache"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop-2.0.xsd
http://www.springframework.org/schema/cache https://www.springframework.org/schema/cache/spring-cache-3.1.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context-2.5.xsd">
<aop:aspectj-autoproxy/>
<context:spring-configured/>
<cache:annotation-driven mode="aspectj"/>
<bean id="cacheManager" class="org.springframework.cache.support.NoOpCacheManager"/>
<bean id="myAspect" class="org.springframework.aop.aspectj.autoproxy.CodeStyleAspect" factory-method="aspectOf">
<property name="foo" value="bar"/>
</bean>
<bean id="otherBean" class="java.lang.Object"/>
<bean id="otherBean" class="org.springframework.beans.factory.aspectj.ShouldBeConfiguredBySpring"/>
<bean id="yetAnotherBean" class="java.lang.Object"/>
<bean id="yetAnotherBean" class="org.springframework.beans.factory.aspectj.ShouldBeConfiguredBySpring"/>
<bean id="configuredBean" class="org.springframework.beans.factory.aspectj.ShouldBeConfiguredBySpring" lazy-init="true"/>
</beans>
@@ -24,8 +24,7 @@
</property>
</bean>
<bean id="defaultCache"
class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean">
<bean id="defaultCache" class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean">
<property name="name" value="default"/>
</bean>
@@ -7,12 +7,10 @@
http://www.springframework.org/schema/task
https://www.springframework.org/schema/task/spring-task.xsd">
<task:annotation-driven mode="aspectj" executor="testExecutor"
exception-handler="testExceptionHandler"/>
<task:annotation-driven mode="aspectj" executor="testExecutor" exception-handler="testExceptionHandler"/>
<task:executor id="testExecutor"/>
<bean id="testExceptionHandler"
class="org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler"/>
<bean id="testExceptionHandler" class="org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler"/>
</beans>
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -42,6 +42,8 @@ import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.beans.testfixture.beans.DerivedTestBean;
import org.springframework.beans.testfixture.beans.ITestBean;
import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceEditor;
import org.springframework.lang.Nullable;
@@ -52,7 +54,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
import static org.assertj.core.api.SoftAssertions.assertSoftly;
/**
* Unit tests for {@link BeanUtils}.
* Tests for {@link BeanUtils}.
*
* @author Juergen Hoeller
* @author Rob Harrop
@@ -136,7 +138,7 @@ class BeanUtilsTests {
PropertyDescriptor[] actual = Introspector.getBeanInfo(TestBean.class).getPropertyDescriptors();
PropertyDescriptor[] descriptors = BeanUtils.getPropertyDescriptors(TestBean.class);
assertThat(descriptors).as("Descriptors should not be null").isNotNull();
assertThat(descriptors.length).as("Invalid number of descriptors returned").isEqualTo(actual.length);
assertThat(descriptors).as("Invalid number of descriptors returned").hasSameSizeAs(actual);
}
@Test
@@ -162,13 +164,13 @@ class BeanUtilsTests {
tb.setAge(32);
tb.setTouchy("touchy");
TestBean tb2 = new TestBean();
assertThat(tb2.getName() == null).as("Name empty").isTrue();
assertThat(tb2.getAge() == 0).as("Age empty").isTrue();
assertThat(tb2.getTouchy() == null).as("Touchy empty").isTrue();
assertThat(tb2.getName()).as("Name empty").isNull();
assertThat(tb2.getAge()).as("Age empty").isEqualTo(0);
assertThat(tb2.getTouchy()).as("Touchy empty").isNull();
BeanUtils.copyProperties(tb, tb2);
assertThat(tb2.getName().equals(tb.getName())).as("Name copied").isTrue();
assertThat(tb2.getAge() == tb.getAge()).as("Age copied").isTrue();
assertThat(tb2.getTouchy().equals(tb.getTouchy())).as("Touchy copied").isTrue();
assertThat(tb2.getName()).as("Name copied").isEqualTo(tb.getName());
assertThat(tb2.getAge()).as("Age copied").isEqualTo(tb.getAge());
assertThat(tb2.getTouchy()).as("Touchy copied").isEqualTo(tb.getTouchy());
}
@Test
@@ -178,13 +180,13 @@ class BeanUtilsTests {
tb.setAge(32);
tb.setTouchy("touchy");
TestBean tb2 = new TestBean();
assertThat(tb2.getName() == null).as("Name empty").isTrue();
assertThat(tb2.getAge() == 0).as("Age empty").isTrue();
assertThat(tb2.getTouchy() == null).as("Touchy empty").isTrue();
assertThat(tb2.getName()).as("Name empty").isNull();
assertThat(tb2.getAge()).as("Age empty").isEqualTo(0);
assertThat(tb2.getTouchy()).as("Touchy empty").isNull();
BeanUtils.copyProperties(tb, tb2);
assertThat(tb2.getName().equals(tb.getName())).as("Name copied").isTrue();
assertThat(tb2.getAge() == tb.getAge()).as("Age copied").isTrue();
assertThat(tb2.getTouchy().equals(tb.getTouchy())).as("Touchy copied").isTrue();
assertThat(tb2.getName()).as("Name copied").isEqualTo(tb.getName());
assertThat(tb2.getAge()).as("Age copied").isEqualTo(tb.getAge());
assertThat(tb2.getTouchy()).as("Touchy copied").isEqualTo(tb.getTouchy());
}
@Test
@@ -194,13 +196,13 @@ class BeanUtilsTests {
tb.setAge(32);
tb.setTouchy("touchy");
DerivedTestBean tb2 = new DerivedTestBean();
assertThat(tb2.getName() == null).as("Name empty").isTrue();
assertThat(tb2.getAge() == 0).as("Age empty").isTrue();
assertThat(tb2.getTouchy() == null).as("Touchy empty").isTrue();
assertThat(tb2.getName()).as("Name empty").isNull();
assertThat(tb2.getAge()).as("Age empty").isEqualTo(0);
assertThat(tb2.getTouchy()).as("Touchy empty").isNull();
BeanUtils.copyProperties(tb, tb2);
assertThat(tb2.getName().equals(tb.getName())).as("Name copied").isTrue();
assertThat(tb2.getAge() == tb.getAge()).as("Age copied").isTrue();
assertThat(tb2.getTouchy().equals(tb.getTouchy())).as("Touchy copied").isTrue();
assertThat(tb2.getName()).as("Name copied").isEqualTo(tb.getName());
assertThat(tb2.getAge()).as("Age copied").isEqualTo(tb.getAge());
assertThat(tb2.getTouchy()).as("Touchy copied").isEqualTo(tb.getTouchy());
}
/**
@@ -227,8 +229,8 @@ class BeanUtilsTests {
IntegerListHolder2 integerListHolder2 = new IntegerListHolder2();
BeanUtils.copyProperties(integerListHolder1, integerListHolder2);
assertThat(integerListHolder1.getList()).containsOnly(42);
assertThat(integerListHolder2.getList()).containsOnly(42);
assertThat(integerListHolder1.getList()).containsExactly(42);
assertThat(integerListHolder2.getList()).containsExactly(42);
}
/**
@@ -257,7 +259,7 @@ class BeanUtilsTests {
WildcardListHolder2 wildcardListHolder2 = new WildcardListHolder2();
BeanUtils.copyProperties(integerListHolder1, wildcardListHolder2);
assertThat(integerListHolder1.getList()).containsOnly(42);
assertThat(integerListHolder1.getList()).containsExactly(42);
assertThat(wildcardListHolder2.getList()).isEqualTo(Arrays.asList(42));
}
@@ -271,9 +273,8 @@ class BeanUtilsTests {
NumberUpperBoundedWildcardListHolder numberListHolder = new NumberUpperBoundedWildcardListHolder();
BeanUtils.copyProperties(integerListHolder1, numberListHolder);
assertThat(integerListHolder1.getList()).containsOnly(42);
assertThat(numberListHolder.getList()).hasSize(1);
assertThat(numberListHolder.getList().contains(Integer.valueOf(42))).isTrue();
assertThat(integerListHolder1.getList()).containsExactly(42);
assertThat(numberListHolder.getList()).isEqualTo(Arrays.asList(42));
}
/**
@@ -282,7 +283,7 @@ class BeanUtilsTests {
@Test
void copyPropertiesDoesNotCopyFromSuperTypeToSubType() {
NumberHolder numberHolder = new NumberHolder();
numberHolder.setNumber(Integer.valueOf(42));
numberHolder.setNumber(42);
IntegerHolder integerHolder = new IntegerHolder();
BeanUtils.copyProperties(numberHolder, integerHolder);
@@ -300,7 +301,7 @@ class BeanUtilsTests {
LongListHolder longListHolder = new LongListHolder();
BeanUtils.copyProperties(integerListHolder, longListHolder);
assertThat(integerListHolder.getList()).containsOnly(42);
assertThat(integerListHolder.getList()).containsExactly(42);
assertThat(longListHolder.getList()).isEmpty();
}
@@ -314,7 +315,7 @@ class BeanUtilsTests {
NumberListHolder numberListHolder = new NumberListHolder();
BeanUtils.copyProperties(integerListHolder, numberListHolder);
assertThat(integerListHolder.getList()).containsOnly(42);
assertThat(integerListHolder.getList()).containsExactly(42);
assertThat(numberListHolder.getList()).isEmpty();
}
@@ -323,12 +324,13 @@ class BeanUtilsTests {
Order original = new Order("test", Arrays.asList("foo", "bar"));
// Create a Proxy that loses the generic type information for the getLineItems() method.
OrderSummary proxy = proxyOrder(original);
OrderSummary proxy = (OrderSummary) Proxy.newProxyInstance(getClass().getClassLoader(),
new Class<?>[] {OrderSummary.class}, new OrderInvocationHandler(original));
assertThat(OrderSummary.class.getDeclaredMethod("getLineItems").toGenericString())
.contains("java.util.List<java.lang.String>");
.contains("java.util.List<java.lang.String>");
assertThat(proxy.getClass().getDeclaredMethod("getLineItems").toGenericString())
.contains("java.util.List")
.doesNotContain("<java.lang.String>");
.contains("java.util.List")
.doesNotContain("<java.lang.String>");
// Ensure that our custom Proxy works as expected.
assertThat(proxy.getId()).isEqualTo("test");
@@ -341,40 +343,57 @@ class BeanUtilsTests {
assertThat(target.getLineItems()).containsExactly("foo", "bar");
}
@Test // gh-32888
public void copyPropertiesWithGenericCglibClass() {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(User.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> proxy.invokeSuper(obj, args));
User user = (User) enhancer.create();
user.setId(1);
user.setName("proxy");
user.setAddress("addr");
User target = new User();
BeanUtils.copyProperties(user, target);
assertThat(target.getId()).isEqualTo(user.getId());
assertThat(target.getName()).isEqualTo(user.getName());
assertThat(target.getAddress()).isEqualTo(user.getAddress());
}
@Test
void copyPropertiesWithEditable() throws Exception {
TestBean tb = new TestBean();
assertThat(tb.getName() == null).as("Name empty").isTrue();
assertThat(tb.getName()).as("Name empty").isNull();
tb.setAge(32);
tb.setTouchy("bla");
TestBean tb2 = new TestBean();
tb2.setName("rod");
assertThat(tb2.getAge() == 0).as("Age empty").isTrue();
assertThat(tb2.getTouchy() == null).as("Touchy empty").isTrue();
assertThat(tb2.getAge()).as("Age empty").isEqualTo(0);
assertThat(tb2.getTouchy()).as("Touchy empty").isNull();
// "touchy" should not be copied: it's not defined in ITestBean
BeanUtils.copyProperties(tb, tb2, ITestBean.class);
assertThat(tb2.getName() == null).as("Name copied").isTrue();
assertThat(tb2.getAge() == 32).as("Age copied").isTrue();
assertThat(tb2.getTouchy() == null).as("Touchy still empty").isTrue();
assertThat(tb2.getName()).as("Name copied").isNull();
assertThat(tb2.getAge()).as("Age copied").isEqualTo(32);
assertThat(tb2.getTouchy()).as("Touchy still empty").isNull();
}
@Test
void copyPropertiesWithIgnore() throws Exception {
TestBean tb = new TestBean();
assertThat(tb.getName() == null).as("Name empty").isTrue();
assertThat(tb.getName()).as("Name empty").isNull();
tb.setAge(32);
tb.setTouchy("bla");
TestBean tb2 = new TestBean();
tb2.setName("rod");
assertThat(tb2.getAge() == 0).as("Age empty").isTrue();
assertThat(tb2.getTouchy() == null).as("Touchy empty").isTrue();
assertThat(tb2.getAge()).as("Age empty").isEqualTo(0);
assertThat(tb2.getTouchy()).as("Touchy empty").isNull();
// "spouse", "touchy", "age" should not be copied
BeanUtils.copyProperties(tb, tb2, "spouse", "touchy", "age");
assertThat(tb2.getName() == null).as("Name copied").isTrue();
assertThat(tb2.getAge() == 0).as("Age still empty").isTrue();
assertThat(tb2.getTouchy() == null).as("Touchy still empty").isTrue();
assertThat(tb2.getName()).as("Name copied").isNull();
assertThat(tb2.getAge()).as("Age still empty").isEqualTo(0);
assertThat(tb2.getTouchy()).as("Touchy still empty").isNull();
}
@Test
@@ -383,7 +402,7 @@ class BeanUtilsTests {
source.setName("name");
TestBean target = new TestBean();
BeanUtils.copyProperties(source, target, "specialProperty");
assertThat("name").isEqualTo(target.getName());
assertThat(target.getName()).isEqualTo("name");
}
@Test
@@ -520,6 +539,7 @@ class BeanUtilsTests {
}
}
@SuppressWarnings("unused")
private static class IntegerHolder {
@@ -534,6 +554,7 @@ class BeanUtilsTests {
}
}
@SuppressWarnings("unused")
private static class WildcardListHolder1 {
@@ -548,6 +569,7 @@ class BeanUtilsTests {
}
}
@SuppressWarnings("unused")
private static class WildcardListHolder2 {
@@ -562,6 +584,7 @@ class BeanUtilsTests {
}
}
@SuppressWarnings("unused")
private static class NumberUpperBoundedWildcardListHolder {
@@ -576,6 +599,7 @@ class BeanUtilsTests {
}
}
@SuppressWarnings("unused")
private static class NumberListHolder {
@@ -590,6 +614,7 @@ class BeanUtilsTests {
}
}
@SuppressWarnings("unused")
private static class IntegerListHolder1 {
@@ -604,6 +629,7 @@ class BeanUtilsTests {
}
}
@SuppressWarnings("unused")
private static class IntegerListHolder2 {
@@ -618,6 +644,7 @@ class BeanUtilsTests {
}
}
@SuppressWarnings("unused")
private static class LongListHolder {
@@ -798,6 +825,7 @@ class BeanUtilsTests {
}
}
private static class BeanWithNullableTypes {
private Integer counter;
@@ -828,6 +856,7 @@ class BeanUtilsTests {
}
}
private static class BeanWithPrimitiveTypes {
private boolean flag;
@@ -840,7 +869,6 @@ class BeanUtilsTests {
private char character;
private String text;
@SuppressWarnings("unused")
public BeanWithPrimitiveTypes(boolean flag, byte byteCount, short shortCount, int intCount, long longCount,
float floatCount, double doubleCount, char character, String text) {
@@ -891,21 +919,22 @@ class BeanUtilsTests {
public String getText() {
return text;
}
}
private static class PrivateBeanWithPrivateConstructor {
private PrivateBeanWithPrivateConstructor() {
}
}
@SuppressWarnings("unused")
private static class Order {
private String id;
private List<String> lineItems;
private List<String> lineItems;
Order() {
}
@@ -937,6 +966,7 @@ class BeanUtilsTests {
}
}
private interface OrderSummary {
String getId();
@@ -945,17 +975,10 @@ class BeanUtilsTests {
}
private OrderSummary proxyOrder(Order order) {
return (OrderSummary) Proxy.newProxyInstance(getClass().getClassLoader(),
new Class<?>[] { OrderSummary.class }, new OrderInvocationHandler(order));
}
private static class OrderInvocationHandler implements InvocationHandler {
private final Order order;
OrderInvocationHandler(Order order) {
this.order = order;
}
@@ -973,4 +996,46 @@ class BeanUtilsTests {
}
}
private static class GenericBaseModel<T> {
private T id;
private String name;
public T getId() {
return id;
}
public void setId(T id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
private static class User extends GenericBaseModel<Integer> {
private String address;
public User() {
super();
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -45,7 +45,11 @@ import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Default implementation of the {@link LifecycleProcessor} strategy.
* Spring's default implementation of the {@link LifecycleProcessor} strategy.
*
* <p>Provides interaction with {@link Lifecycle} and {@link SmartLifecycle} beans in
* groups for specific phases, on startup/shutdown as well as for explicit start/stop
* interactions on a {@link org.springframework.context.ConfigurableApplicationContext}.
*
* @author Mark Fisher
* @author Juergen Hoeller
@@ -145,13 +149,13 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
lifecycleBeans.forEach((beanName, bean) -> {
if (!autoStartupOnly || (bean instanceof SmartLifecycle && ((SmartLifecycle) bean).isAutoStartup())) {
int phase = getPhase(bean);
phases.computeIfAbsent(
phase,
p -> new LifecycleGroup(phase, this.timeoutPerShutdownPhase, lifecycleBeans, autoStartupOnly)
int startupPhase = getPhase(bean);
phases.computeIfAbsent(startupPhase,
phase -> new LifecycleGroup(phase, this.timeoutPerShutdownPhase, lifecycleBeans, autoStartupOnly)
).add(beanName, bean);
}
});
if (!phases.isEmpty()) {
phases.values().forEach(LifecycleGroup::start);
}
@@ -191,6 +195,7 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
private void stopBeans() {
Map<String, Lifecycle> lifecycleBeans = getLifecycleBeans();
Map<Integer, LifecycleGroup> phases = new HashMap<>();
lifecycleBeans.forEach((beanName, bean) -> {
int shutdownPhase = getPhase(bean);
LifecycleGroup group = phases.get(shutdownPhase);
@@ -200,6 +205,7 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
}
group.add(beanName, bean);
});
if (!phases.isEmpty()) {
List<Integer> keys = new ArrayList<>(phases.keySet());
keys.sort(Collections.reverseOrder());
@@ -314,6 +320,8 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
/**
* Helper class for maintaining a group of Lifecycle beans that should be started
* and stopped together based on their 'phase' value (or the default value of 0).
* The group is expected to be created in an ad-hoc fashion and group members are
* expected to always have the same 'phase' value.
*/
private class LifecycleGroup {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -57,11 +57,11 @@ public final class BridgeMethodResolver {
/**
* Find the original method for the supplied {@link Method bridge Method}.
* Find the local original method for the supplied {@link Method bridge Method}.
* <p>It is safe to call this method passing in a non-bridge {@link Method} instance.
* In such a case, the supplied {@link Method} instance is returned directly to the caller.
* Callers are <strong>not</strong> required to check for bridging before calling this method.
* @param bridgeMethod the method to introspect
* @param bridgeMethod the method to introspect against its declaring class
* @return the original method (either the bridged method or the passed-in method
* if no more specific one could be found)
*/
@@ -73,8 +73,7 @@ public final class BridgeMethodResolver {
if (bridgedMethod == null) {
// Gather all methods with matching name and parameter size.
List<Method> candidateMethods = new ArrayList<>();
MethodFilter filter = candidateMethod ->
isBridgedCandidateFor(candidateMethod, bridgeMethod);
MethodFilter filter = (candidateMethod -> isBridgedCandidateFor(candidateMethod, bridgeMethod));
ReflectionUtils.doWithMethods(bridgeMethod.getDeclaringClass(), candidateMethods::add, filter);
if (!candidateMethods.isEmpty()) {
bridgedMethod = candidateMethods.size() == 1 ?
@@ -95,10 +94,10 @@ public final class BridgeMethodResolver {
* Returns {@code true} if the supplied '{@code candidateMethod}' can be
* considered a valid candidate for the {@link Method} that is {@link Method#isBridge() bridged}
* by the supplied {@link Method bridge Method}. This method performs inexpensive
* checks and can be used quickly to filter for a set of possible matches.
* checks and can be used to quickly filter for a set of possible matches.
*/
private static boolean isBridgedCandidateFor(Method candidateMethod, Method bridgeMethod) {
return (!candidateMethod.isBridge() && !candidateMethod.equals(bridgeMethod) &&
return (!candidateMethod.isBridge() &&
candidateMethod.getName().equals(bridgeMethod.getName()) &&
candidateMethod.getParameterCount() == bridgeMethod.getParameterCount());
}
@@ -121,8 +120,8 @@ public final class BridgeMethodResolver {
return candidateMethod;
}
else if (previousMethod != null) {
sameSig = sameSig &&
Arrays.equals(candidateMethod.getGenericParameterTypes(), previousMethod.getGenericParameterTypes());
sameSig = sameSig && Arrays.equals(
candidateMethod.getGenericParameterTypes(), previousMethod.getGenericParameterTypes());
}
previousMethod = candidateMethod;
}
@@ -163,7 +162,8 @@ public final class BridgeMethodResolver {
}
}
// A non-array type: compare the type itself.
if (!ClassUtils.resolvePrimitiveIfNecessary(candidateParameter).equals(ClassUtils.resolvePrimitiveIfNecessary(genericParameter.toClass()))) {
if (!ClassUtils.resolvePrimitiveIfNecessary(candidateParameter).equals(
ClassUtils.resolvePrimitiveIfNecessary(genericParameter.toClass()))) {
return false;
}
}
@@ -226,8 +226,8 @@ public final class BridgeMethodResolver {
/**
* Compare the signatures of the bridge method and the method which it bridges. If
* the parameter and return types are the same, it is a 'visibility' bridge method
* introduced in Java 6 to fix https://bugs.openjdk.org/browse/JDK-6342411.
* See also https://stas-blogspot.blogspot.com/2010/03/java-bridge-methods-explained.html
* introduced in Java 6 to fix <a href="https://bugs.openjdk.org/browse/JDK-6342411">
* JDK-6342411</a>.
* @return whether signatures match as described
*/
public static boolean isVisibilityBridgeMethodPair(Method bridgeMethod, Method bridgedMethod) {
@@ -1052,16 +1052,16 @@ public abstract class AnnotationUtils {
return null;
}
try {
Method method = annotation.annotationType().getDeclaredMethod(attributeName);
return invokeAnnotationMethod(method, annotation);
}
catch (NoSuchMethodException ex) {
return null;
for (Method method : annotation.annotationType().getDeclaredMethods()) {
if (method.getName().equals(attributeName) && method.getParameterCount() == 0) {
return invokeAnnotationMethod(method, annotation);
}
}
}
catch (Throwable ex) {
handleValueRetrievalFailure(annotation, ex);
return null;
}
return null;
}
/**
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -226,6 +226,8 @@ public class Indexer extends SpelNodeImpl {
cf.loadTarget(mv);
}
SpelNodeImpl index = this.children[0];
if (this.indexedType == IndexedType.ARRAY) {
int insn;
if ("D".equals(this.exitTypeDescriptor)) {
@@ -262,18 +264,14 @@ public class Indexer extends SpelNodeImpl {
//depthPlusOne(exitTypeDescriptor)+"Ljava/lang/Object;");
insn = AALOAD;
}
SpelNodeImpl index = this.children[0];
cf.enterCompilationScope();
index.generateCode(mv, cf);
cf.exitCompilationScope();
generateIndexCode(mv, cf, index, int.class);
mv.visitInsn(insn);
}
else if (this.indexedType == IndexedType.LIST) {
mv.visitTypeInsn(CHECKCAST, "java/util/List");
cf.enterCompilationScope();
this.children[0].generateCode(mv, cf);
cf.exitCompilationScope();
generateIndexCode(mv, cf, index, int.class);
mv.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "get", "(I)Ljava/lang/Object;", true);
}
@@ -281,15 +279,13 @@ public class Indexer extends SpelNodeImpl {
mv.visitTypeInsn(CHECKCAST, "java/util/Map");
// Special case when the key is an unquoted string literal that will be parsed as
// a property/field reference
if ((this.children[0] instanceof PropertyOrFieldReference)) {
if (index instanceof PropertyOrFieldReference) {
PropertyOrFieldReference reference = (PropertyOrFieldReference) this.children[0];
String mapKeyName = reference.getName();
mv.visitLdcInsn(mapKeyName);
}
else {
cf.enterCompilationScope();
this.children[0].generateCode(mv, cf);
cf.exitCompilationScope();
generateIndexCode(mv, cf, index, Object.class);
}
mv.visitMethodInsn(
INVOKEINTERFACE, "java/util/Map", "get", "(Ljava/lang/Object;)Ljava/lang/Object;", true);
@@ -325,6 +321,11 @@ public class Indexer extends SpelNodeImpl {
cf.pushDescriptor(this.exitTypeDescriptor);
}
private void generateIndexCode(MethodVisitor mv, CodeFlow cf, SpelNodeImpl indexNode, Class<?> indexType) {
String indexDesc = CodeFlow.toDescriptor(indexType);
generateCodeForArgument(mv, cf, indexNode, indexDesc);
}
@Override
public String toStringAST() {
StringJoiner sj = new StringJoiner(",", "[", "]");
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,6 +20,8 @@ import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -27,7 +29,10 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.asm.MethodVisitor;
@@ -55,6 +60,7 @@ import static org.assertj.core.api.InstanceOfAssertFactories.BOOLEAN;
* Checks SpelCompiler behavior. This should cover compilation all compiled node types.
*
* @author Andy Clement
* @author Sam Brannen
* @since 4.1
*/
public class SpelCompilationCoverageTests extends AbstractExpressionTests {
@@ -129,6 +135,488 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
private SpelNodeImpl ast;
@Nested
class VariableReferenceTests {
@Test
void userDefinedVariable() {
EvaluationContext ctx = new StandardEvaluationContext();
ctx.setVariable("target", "abc");
expression = parser.parseExpression("#target");
assertThat(expression.getValue(ctx)).isEqualTo("abc");
assertCanCompile(expression);
assertThat(expression.getValue(ctx)).isEqualTo("abc");
ctx.setVariable("target", "123");
assertThat(expression.getValue(ctx)).isEqualTo("123");
// Changing the variable type from String to Integer results in a
// ClassCastException in the compiled code.
ctx.setVariable("target", 42);
assertThatExceptionOfType(SpelEvaluationException.class)
.isThrownBy(() -> expression.getValue(ctx))
.withCauseInstanceOf(ClassCastException.class);
ctx.setVariable("target", "abc");
expression = parser.parseExpression("#target.charAt(0)");
assertThat(expression.getValue(ctx)).isEqualTo('a');
assertCanCompile(expression);
assertThat(expression.getValue(ctx)).isEqualTo('a');
ctx.setVariable("target", "1");
assertThat(expression.getValue(ctx)).isEqualTo('1');
// Changing the variable type from String to Integer results in a
// ClassCastException in the compiled code.
ctx.setVariable("target", 42);
assertThatExceptionOfType(SpelEvaluationException.class)
.isThrownBy(() -> expression.getValue(ctx))
.withCauseInstanceOf(ClassCastException.class);
}
}
@Nested
class IndexingTests {
@Test
void indexIntoPrimitiveShortArray() {
short[] shorts = { (short) 33, (short) 44, (short) 55 };
expression = parser.parseExpression("[2]");
assertThat(expression.getValue(shorts)).isEqualTo((short) 55);
assertCanCompile(expression);
assertThat(expression.getValue(shorts)).isEqualTo((short) 55);
assertThat(getAst().getExitDescriptor()).isEqualTo("S");
}
@Test
void indexIntoPrimitiveByteArray() {
byte[] bytes = { (byte) 2, (byte) 3, (byte) 4 };
expression = parser.parseExpression("[2]");
assertThat(expression.getValue(bytes)).isEqualTo((byte) 4);
assertCanCompile(expression);
assertThat(expression.getValue(bytes)).isEqualTo((byte) 4);
assertThat(getAst().getExitDescriptor()).isEqualTo("B");
}
@Test
void indexIntoPrimitiveIntArray() {
int[] ints = { 8, 9, 10 };
expression = parser.parseExpression("[2]");
assertThat(expression.getValue(ints)).isEqualTo(10);
assertCanCompile(expression);
assertThat(expression.getValue(ints)).isEqualTo(10);
assertThat(getAst().getExitDescriptor()).isEqualTo("I");
}
@Test
void indexIntoPrimitiveLongArray() {
long[] longs = { 2L, 3L, 4L };
expression = parser.parseExpression("[0]");
assertThat(expression.getValue(longs)).isEqualTo(2L);
assertCanCompile(expression);
assertThat(expression.getValue(longs)).isEqualTo(2L);
assertThat(getAst().getExitDescriptor()).isEqualTo("J");
}
@Test
void indexIntoPrimitiveFloatArray() {
float[] floats = { 6.0f, 7.0f, 8.0f };
expression = parser.parseExpression("[0]");
assertThat(expression.getValue(floats)).isEqualTo(6.0f);
assertCanCompile(expression);
assertThat(expression.getValue(floats)).isEqualTo(6.0f);
assertThat(getAst().getExitDescriptor()).isEqualTo("F");
}
@Test
void indexIntoPrimitiveDoubleArray() {
double[] doubles = { 3.0d, 4.0d, 5.0d };
expression = parser.parseExpression("[1]");
assertThat(expression.getValue(doubles)).isEqualTo(4.0d);
assertCanCompile(expression);
assertThat(expression.getValue(doubles)).isEqualTo(4.0d);
assertThat(getAst().getExitDescriptor()).isEqualTo("D");
}
@Test
void indexIntoPrimitiveCharArray() {
char[] chars = { 'a', 'b', 'c' };
expression = parser.parseExpression("[1]");
assertThat(expression.getValue(chars)).isEqualTo('b');
assertCanCompile(expression);
assertThat(expression.getValue(chars)).isEqualTo('b');
assertThat(getAst().getExitDescriptor()).isEqualTo("C");
}
@Test
void indexIntoStringArray() {
String[] strings = { "a", "b", "c" };
expression = parser.parseExpression("[0]");
assertThat(expression.getValue(strings)).isEqualTo("a");
assertCanCompile(expression);
assertThat(expression.getValue(strings)).isEqualTo("a");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String");
}
@Test
void indexIntoNumberArray() {
Number[] numbers = { 2, 8, 9 };
expression = parser.parseExpression("[1]");
assertThat(expression.getValue(numbers)).isEqualTo(8);
assertCanCompile(expression);
assertThat(expression.getValue(numbers)).isEqualTo(8);
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Number");
}
@Test
void indexInto2DPrimitiveIntArray() {
int[][] array = new int[][] {
{ 1, 2, 3 },
{ 4, 5, 6 }
};
expression = parser.parseExpression("[1]");
assertThat(stringify(expression.getValue(array))).isEqualTo("4 5 6");
assertCanCompile(expression);
assertThat(stringify(expression.getValue(array))).isEqualTo("4 5 6");
assertThat(getAst().getExitDescriptor()).isEqualTo("[I");
expression = parser.parseExpression("[1][2]");
assertThat(stringify(expression.getValue(array))).isEqualTo("6");
assertCanCompile(expression);
assertThat(stringify(expression.getValue(array))).isEqualTo("6");
assertThat(getAst().getExitDescriptor()).isEqualTo("I");
}
@Test
void indexInto2DStringArray() {
String[][] array = new String[][] {
{ "a", "b", "c" },
{ "d", "e", "f" }
};
expression = parser.parseExpression("[1]");
assertThat(stringify(expression.getValue(array))).isEqualTo("d e f");
assertCanCompile(expression);
assertThat(getAst().getExitDescriptor()).isEqualTo("[Ljava/lang/String");
assertThat(stringify(expression.getValue(array))).isEqualTo("d e f");
assertThat(getAst().getExitDescriptor()).isEqualTo("[Ljava/lang/String");
expression = parser.parseExpression("[1][2]");
assertThat(stringify(expression.getValue(array))).isEqualTo("f");
assertCanCompile(expression);
assertThat(stringify(expression.getValue(array))).isEqualTo("f");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String");
}
@Test
@SuppressWarnings("unchecked")
void indexIntoArrayOfListOfString() {
List<String>[] array = new List[] {
Arrays.asList("a", "b", "c"),
Arrays.asList("d", "e", "f")
};
expression = parser.parseExpression("[1]");
assertThat(stringify(expression.getValue(array))).isEqualTo("d e f");
assertCanCompile(expression);
assertThat(stringify(expression.getValue(array))).isEqualTo("d e f");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/util/List");
expression = parser.parseExpression("[1][2]");
assertThat(stringify(expression.getValue(array))).isEqualTo("f");
assertCanCompile(expression);
assertThat(stringify(expression.getValue(array))).isEqualTo("f");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
}
@Test
@SuppressWarnings("unchecked")
void indexIntoArrayOfMap() {
Map<String, String>[] array = new Map[] { Collections.singletonMap("key", "value1") };
expression = parser.parseExpression("[0]");
assertThat(stringify(expression.getValue(array))).isEqualTo("{key=value1}");
assertCanCompile(expression);
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/util/Map");
assertThat(stringify(expression.getValue(array))).isEqualTo("{key=value1}");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/util/Map");
expression = parser.parseExpression("[0]['key']");
assertThat(stringify(expression.getValue(array))).isEqualTo("value1");
assertCanCompile(expression);
assertThat(stringify(expression.getValue(array))).isEqualTo("value1");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
}
@Test
void indexIntoListOfString() {
List<String> list = Arrays.asList("aaa", "bbb", "ccc");
expression = parser.parseExpression("[1]");
assertThat(expression.getValue(list)).isEqualTo("bbb");
assertCanCompile(expression);
assertThat(expression.getValue(list)).isEqualTo("bbb");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
}
@Test
void indexIntoListOfInteger() {
List<Integer> list = Arrays.asList(123, 456, 789);
expression = parser.parseExpression("[2]");
assertThat(expression.getValue(list)).isEqualTo(789);
assertCanCompile(expression);
assertThat(expression.getValue(list)).isEqualTo(789);
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
}
@Test
void indexIntoListOfStringArray() {
List<String[]> list = Arrays.asList(
new String[] { "a", "b", "c" },
new String[] { "d", "e", "f" }
);
expression = parser.parseExpression("[1]");
assertThat(stringify(expression.getValue(list))).isEqualTo("d e f");
assertCanCompile(expression);
assertThat(stringify(expression.getValue(list))).isEqualTo("d e f");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
expression = parser.parseExpression("[1][0]");
assertThat(stringify(expression.getValue(list))).isEqualTo("d");
assertCanCompile(expression);
assertThat(stringify(expression.getValue(list))).isEqualTo("d");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String");
}
@Test
void indexIntoListOfIntegerArray() {
List<Integer[]> list = Arrays.asList(
new Integer[] { 1, 2, 3 },
new Integer[] { 4, 5, 6 }
);
expression = parser.parseExpression("[0]");
assertThat(stringify(expression.getValue(list))).isEqualTo("1 2 3");
assertCanCompile(expression);
assertThat(stringify(expression.getValue(list))).isEqualTo("1 2 3");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
expression = parser.parseExpression("[0][1]");
assertThat(expression.getValue(list)).isEqualTo(2);
assertCanCompile(expression);
assertThat(expression.getValue(list)).isEqualTo(2);
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Integer");
}
@Test
void indexIntoListOfListOfString() {
List<List<String>> list = Arrays.asList(
Arrays.asList("a", "b", "c"),
Arrays.asList("d", "e", "f")
);
expression = parser.parseExpression("[1]");
assertThat(stringify(expression.getValue(list))).isEqualTo("d e f");
assertCanCompile(expression);
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
assertThat(stringify(expression.getValue(list))).isEqualTo("d e f");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
expression = parser.parseExpression("[1][2]");
assertThat(stringify(expression.getValue(list))).isEqualTo("f");
assertCanCompile(expression);
assertThat(stringify(expression.getValue(list))).isEqualTo("f");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
}
@Test
void indexIntoMap() {
Map<String, Integer> map = Collections.singletonMap("aaa", 111);
expression = parser.parseExpression("['aaa']");
assertThat(expression.getValue(map)).isEqualTo(111);
assertCanCompile(expression);
assertThat(expression.getValue(map)).isEqualTo(111);
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
}
@Test
void indexIntoMapOfListOfString() {
Map<String, List<String>> map = Collections.singletonMap("foo", Arrays.asList("a", "b", "c"));
expression = parser.parseExpression("['foo']");
assertThat(stringify(expression.getValue(map))).isEqualTo("a b c");
assertCanCompile(expression);
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
assertThat(stringify(expression.getValue(map))).isEqualTo("a b c");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
expression = parser.parseExpression("['foo'][2]");
assertThat(stringify(expression.getValue(map))).isEqualTo("c");
assertCanCompile(expression);
assertThat(stringify(expression.getValue(map))).isEqualTo("c");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
}
@Test
void indexIntoObject() {
TestClass6 tc = new TestClass6();
// field access
expression = parser.parseExpression("['orange']");
assertThat(expression.getValue(tc)).isEqualTo("value1");
assertCanCompile(expression);
assertThat(expression.getValue(tc)).isEqualTo("value1");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String");
// field access
expression = parser.parseExpression("['peach']");
assertThat(expression.getValue(tc)).isEqualTo(34L);
assertCanCompile(expression);
assertThat(expression.getValue(tc)).isEqualTo(34L);
assertThat(getAst().getExitDescriptor()).isEqualTo("J");
// property access (getter)
expression = parser.parseExpression("['banana']");
assertThat(expression.getValue(tc)).isEqualTo("value3");
assertCanCompile(expression);
assertThat(expression.getValue(tc)).isEqualTo("value3");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String");
}
@Test // gh-32694, gh-32908
void indexIntoArrayUsingIntegerWrapper() {
context.setVariable("array", new int[] {1, 2, 3, 4});
context.setVariable("index", 2);
expression = parser.parseExpression("#array[#index]");
assertThat(expression.getValue(context)).isEqualTo(3);
assertCanCompile(expression);
assertThat(expression.getValue(context)).isEqualTo(3);
assertThat(getAst().getExitDescriptor()).isEqualTo("I");
}
@Test // gh-32694, gh-32908
void indexIntoListUsingIntegerWrapper() {
context.setVariable("list", Arrays.asList(1, 2, 3, 4));
context.setVariable("index", 2);
expression = parser.parseExpression("#list[#index]");
assertThat(expression.getValue(context)).isEqualTo(3);
assertCanCompile(expression);
assertThat(expression.getValue(context)).isEqualTo(3);
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
}
@Test // gh-32903
void indexIntoMapUsingPrimitiveLiteral() {
Map<Object, String> map = new HashMap<>();
map.put(false, "0"); // BooleanLiteral
map.put(1, "ABC"); // IntLiteral
map.put(2L, "XYZ"); // LongLiteral
map.put(9.99F, "~10"); // FloatLiteral
map.put(3.14159, "PI"); // RealLiteral
context.setVariable("map", map);
// BooleanLiteral
expression = parser.parseExpression("#map[false]");
assertThat(expression.getValue(context)).isEqualTo("0");
assertCanCompile(expression);
assertThat(expression.getValue(context)).isEqualTo("0");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
// IntLiteral
expression = parser.parseExpression("#map[1]");
assertThat(expression.getValue(context)).isEqualTo("ABC");
assertCanCompile(expression);
assertThat(expression.getValue(context)).isEqualTo("ABC");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
// LongLiteral
expression = parser.parseExpression("#map[2L]");
assertThat(expression.getValue(context)).isEqualTo("XYZ");
assertCanCompile(expression);
assertThat(expression.getValue(context)).isEqualTo("XYZ");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
// FloatLiteral
expression = parser.parseExpression("#map[9.99F]");
assertThat(expression.getValue(context)).isEqualTo("~10");
assertCanCompile(expression);
assertThat(expression.getValue(context)).isEqualTo("~10");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
// RealLiteral
expression = parser.parseExpression("#map[3.14159]");
assertThat(expression.getValue(context)).isEqualTo("PI");
assertCanCompile(expression);
assertThat(expression.getValue(context)).isEqualTo("PI");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
}
private String stringify(Object object) {
Stream<? extends Object> stream;
if (object instanceof Collection) {
stream = ((Collection<?>) object).stream();
}
else if (object instanceof Object[]) {
stream = Arrays.stream((Object[]) object);
}
else if (object instanceof int[]) {
stream = Arrays.stream((int[]) object).mapToObj(Integer::valueOf);
}
else {
return String.valueOf(object);
}
return stream.map(Object::toString).collect(Collectors.joining(" "));
}
}
@Test
void typeReference() {
expression = parse("T(String)");